defmodule MvWeb.AuthControllerTest do
  use MvWeb.ConnCase, async: true
  import Phoenix.LiveViewTest
  import Phoenix.ConnTest
  import ExUnit.CaptureLog

  # Helper to create an unauthenticated conn (preserves sandbox metadata)
  defp build_unauthenticated_conn(authenticated_conn) do
    # Create new conn but preserve sandbox metadata for database access
    new_conn =
      build_conn()
      |> init_test_session(%{})
      |> fetch_flash()

    # Copy sandbox metadata from authenticated conn
    if authenticated_conn.private[:ecto_sandbox] do
      Plug.Conn.put_private(new_conn, :ecto_sandbox, authenticated_conn.private[:ecto_sandbox])
    else
      new_conn
    end
  end

  # Basic UI tests
  test "GET /sign-in shows sign in form", %{conn: authenticated_conn} do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)
    conn = get(conn, ~p"/sign-in")
    assert html_response(conn, 200) =~ "Sign in"
  end

  test "GET /sign-out redirects to home", %{conn: authenticated_conn} do
    conn = conn_with_oidc_user(authenticated_conn)
    conn = get(conn, ~p"/sign-out")
    assert redirected_to(conn) == ~p"/"
  end

  # Password authentication (LiveView)
  test "password user can sign in with valid credentials via LiveView", %{
    conn: authenticated_conn
  } do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    _user =
      create_test_user(%{
        email: "password@example.com",
        password: "secret123",
        oidc_id: nil
      })

    {:ok, view, _html} = live(conn, "/sign-in")

    {:error, {:redirect, %{to: to}}} =
      view
      |> form("#user-password-sign-in-with-password",
        user: %{email: "password@example.com", password: "secret123"}
      )
      |> render_submit()

    assert to =~ "/auth/user/password/sign_in_with_token"
  end

  test "password user with invalid credentials shows error via LiveView", %{
    conn: authenticated_conn
  } do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    _user =
      create_test_user(%{
        email: "test@example.com",
        password: "correct_password",
        oidc_id: nil
      })

    {:ok, view, _html} = live(conn, "/sign-in")

    html =
      view
      |> form("#user-password-sign-in-with-password",
        user: %{email: "test@example.com", password: "wrong_password"}
      )
      |> render_submit()

    assert html =~ "Email or password was incorrect"
  end

  test "password user with non-existent email shows error via LiveView", %{
    conn: authenticated_conn
  } do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    {:ok, view, _html} = live(conn, "/sign-in")

    html =
      view
      |> form("#user-password-sign-in-with-password",
        user: %{email: "nonexistent@example.com", password: "anypassword"}
      )
      |> render_submit()

    assert html =~ "Email or password was incorrect"
  end

  # Registration (LiveView)
  test "user can register with valid credentials via LiveView", %{conn: authenticated_conn} do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    {:ok, view, _html} = live(conn, "/register")

    {:error, {:redirect, %{to: to}}} =
      view
      |> form("#user-password-register-with-password-wrapper form",
        user: %{email: "newuser@example.com", password: "newpassword123"}
      )
      |> render_submit()

    assert to =~ "/auth/user/password/sign_in_with_token"
  end

  test "registration with existing email shows error via LiveView", %{conn: authenticated_conn} do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    _user =
      create_test_user(%{
        email: "existing@example.com",
        password: "secret123",
        oidc_id: nil
      })

    {:ok, view, _html} = live(conn, "/register")

    html =
      view
      |> form("#user-password-register-with-password-wrapper form",
        user: %{email: "existing@example.com", password: "anotherpassword"}
      )
      |> render_submit()

    assert html =~ "has already been taken"
  end

  test "registration with weak password shows error via LiveView", %{conn: authenticated_conn} do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    {:ok, view, _html} = live(conn, "/register")

    html =
      view
      |> form("#user-password-register-with-password-wrapper form",
        user: %{email: "weakpass@example.com", password: "123"}
      )
      |> render_submit()

    assert html =~ "length must be greater than or equal to 8"
  end

  # Access control
  test "unauthenticated user accessing protected route gets redirected to sign-in", %{
    conn: authenticated_conn
  } do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)
    conn = get(conn, ~p"/members")
    assert redirected_to(conn) == ~p"/sign-in"
  end

  test "authenticated user can access protected route", %{conn: authenticated_conn} do
    conn = conn_with_oidc_user(authenticated_conn)
    conn = get(conn, ~p"/members")
    assert conn.status == 200
  end

  test "password authenticated user can access protected route via LiveView", %{
    conn: authenticated_conn
  } do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    _user =
      create_test_user(%{
        email: "auth@example.com",
        password: "secret123",
        oidc_id: nil
      })

    {:ok, view, _html} = live(conn, "/sign-in")

    {:error, {:redirect, %{to: to}}} =
      view
      |> form("#user-password-sign-in-with-password",
        user: %{email: "auth@example.com", password: "secret123"}
      )
      |> render_submit()

    assert to =~ "/auth/user/password/sign_in_with_token"

    # After login, user is redirected to /auth/user/password/sign_in_with_token. Session handling for protected routes should be tested in integration or E2E tests.
  end

  # Edge cases
  test "user with nil oidc_id can still sign in with password via LiveView", %{
    conn: authenticated_conn
  } do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    _user =
      create_test_user(%{
        email: "nil_oidc@example.com",
        password: "secret123",
        oidc_id: nil
      })

    {:ok, view, _html} = live(conn, "/sign-in")

    {:error, {:redirect, %{to: to}}} =
      view
      |> form("#user-password-sign-in-with-password",
        user: %{email: "nil_oidc@example.com", password: "secret123"}
      )
      |> render_submit()

    assert to =~ "/auth/user/password/sign_in_with_token"
  end

  test "user with empty string oidc_id is handled correctly via LiveView", %{
    conn: authenticated_conn
  } do
    # Create unauthenticated conn for this test
    conn = build_unauthenticated_conn(authenticated_conn)

    _user =
      create_test_user(%{
        email: "empty_oidc@example.com",
        password: "secret123",
        oidc_id: ""
      })

    {:ok, view, _html} = live(conn, "/sign-in")

    {:error, {:redirect, %{to: to}}} =
      view
      |> form("#user-password-sign-in-with-password",
        user: %{email: "empty_oidc@example.com", password: "secret123"}
      )
      |> render_submit()

    assert to =~ "/auth/user/password/sign_in_with_token"
  end

  # OIDC/Rauthy error handling tests
  describe "handle_rauthy_failure/2" do
    test "Assent.ServerUnreachableError redirects to sign-in with error flash", %{
      conn: authenticated_conn
    } do
      conn = build_unauthenticated_conn(authenticated_conn)
      # Create a mock Assent.ServerUnreachableError struct with required fields
      error = %Assent.ServerUnreachableError{
        http_adapter: Assent.HTTPAdapter.Finch,
        request_url: "https://auth.example.com/callback?token=secret123",
        reason: %Mint.TransportError{reason: :econnrefused}
      }

      conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error)

      assert redirected_to(conn) == ~p"/sign-in"

      assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
               "The authentication server is currently unavailable. Please try again later."
    end

    test "Assent.InvalidResponseError redirects to sign-in with error flash", %{
      conn: authenticated_conn
    } do
      conn = build_unauthenticated_conn(authenticated_conn)
      # Create a mock Assent.InvalidResponseError struct with required field
      # InvalidResponseError only has :response field (HTTPResponse struct)
      error = %Assent.InvalidResponseError{
        response: %Assent.HTTPAdapter.HTTPResponse{
          status: 400,
          headers: [],
          body: "invalid_request"
        }
      }

      conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error)

      assert redirected_to(conn) == ~p"/sign-in"

      assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
               "Authentication configuration error. Please contact the administrator."
    end

    test "unknown reason triggers catch-all and redirects to sign-in with error flash", %{
      conn: authenticated_conn
    } do
      conn = build_unauthenticated_conn(authenticated_conn)
      unknown_reason = :oops

      conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, unknown_reason)

      assert redirected_to(conn) == ~p"/sign-in"

      assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
               "Unable to authenticate with OIDC. Please try again."
    end
  end

  # Logging security tests - ensure no sensitive data is logged
  describe "failure/3 logging security" do
    test "does not log full URL with query params for Assent.ServerUnreachableError", %{
      conn: authenticated_conn
    } do
      conn = build_unauthenticated_conn(authenticated_conn)

      error = %Assent.ServerUnreachableError{
        http_adapter: Assent.HTTPAdapter.Finch,
        request_url: "https://auth.example.com/callback?token=secret123&code=abc456",
        reason: %Mint.TransportError{reason: :econnrefused}
      }

      log =
        capture_log(fn ->
          MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error)
        end)

      # Should log redacted URL (only scheme and host)
      assert log =~ "https://auth.example.com"
      # Should NOT log query parameters or tokens
      refute log =~ "token=secret123"
      refute log =~ "code=abc456"
      refute log =~ "callback?token"
    end

    test "does not log sensitive data for Assent.InvalidResponseError", %{
      conn: authenticated_conn
    } do
      conn = build_unauthenticated_conn(authenticated_conn)

      error = %Assent.InvalidResponseError{
        response: %Assent.HTTPAdapter.HTTPResponse{
          status: 400,
          headers: [],
          body: "invalid_request"
        }
      }

      log =
        capture_log(fn ->
          MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error)
        end)

      # Should log error type but not full error details
      assert log =~ "Authentication failure"
      assert log =~ "rauthy"
      # Should not log full error struct with inspect
      refute log =~ "Assent.InvalidResponseError"
    end

    test "does not log full reason for unknown rauthy errors", %{
      conn: authenticated_conn
    } do
      conn = build_unauthenticated_conn(authenticated_conn)
      # Simulate an error that might contain sensitive data
      error_with_sensitive_data = %{
        token: "secret_token_123",
        url: "https://example.com/callback?access_token=abc123",
        error: :something_went_wrong
      }

      log =
        capture_log(fn ->
          MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error_with_sensitive_data)
        end)

      # Should log error type but not full error details
      assert log =~ "Authentication failure"
      assert log =~ "rauthy"
      # Should NOT log sensitive data
      refute log =~ "secret_token_123"
      refute log =~ "access_token=abc123"
      refute log =~ "callback?access_token"
    end

    test "logs full reason for non-rauthy activities (password auth)", %{
      conn: authenticated_conn
    } do
      conn = build_unauthenticated_conn(authenticated_conn)

      reason = %AshAuthentication.Errors.AuthenticationFailed{
        caused_by: %Ash.Error.Forbidden{errors: []}
      }

      log =
        capture_log(fn ->
          MvWeb.AuthController.failure(conn, {:password, :sign_in}, reason)
        end)

      # For non-rauthy activities, full reason is safe to log
      assert log =~ "Authentication failure"
      assert log =~ "password"
      assert log =~ "AuthenticationFailed"
    end
  end
end
