defmodule WhoNeedHelpWeb.GoogleAuthControllerTest do
  use WhoNeedHelpWeb.ConnCase, async: false

  alias WhoNeedHelp.Accounts
  alias WhoNeedHelp.Accounts.AuthIdentity
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Trust.AuditEvent

  import WhoNeedHelp.AccountsFixtures
  import Swoosh.TestAssertions

  test "starts a login flow with callback state, nonce, and PKCE session data", %{conn: conn} do
    conn = post(conn, ~p"/auth/google/login")

    assert redirected_to(conn) =~ "https://accounts.google.example/authorize?"

    assert %{
             "flow" => "login",
             "session_params" => %{
               state: "google-test-state",
               nonce: "google-test-nonce",
               code_verifier: "google-test-verifier"
             }
           } = get_session(conn, "google_auth_flow")
  end

  test "requires adult and safety consent before starting Google registration", %{conn: conn} do
    conn =
      post(conn, ~p"/auth/google/register", %{
        "google_registration" => %{"locale" => "en", "terms_accepted" => "false"}
      })

    assert redirected_to(conn) == ~p"/users/register"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "18 or older"
    assert is_nil(get_session(conn, "google_auth_flow"))
  end

  test "registers, confirms, links, and logs in a new verified Google user", %{conn: conn} do
    email = unique_user_email()

    conn =
      conn
      |> post(~p"/auth/google/register", %{
        "google_registration" => %{"locale" => "ru", "terms_accepted" => "true"}
      })
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=one-time-code&state=google-test-state&uid=google-123&email=#{email}&name=Google%20Helper"
      )

    assert redirected_to(conn) == ~p"/"
    assert get_session(conn, :user_token)
    assert is_nil(get_session(conn, "google_auth_flow"))

    user = Accounts.get_user_by_email(email)
    assert user.confirmed_at
    assert user.accepted_terms_at
    assert user.locale == "ru"
    assert user.display_name == "Google Helper"
    assert is_nil(user.hashed_password)

    assert %AuthIdentity{
             provider: :google,
             provider_uid: "google-123",
             email: ^email,
             user_id: user_id
           } = Repo.get_by(AuthIdentity, provider: :google, provider_uid: "google-123")

    assert user_id == user.id
  end

  test "requires local-account verification before linking a matching email", %{conn: conn} do
    user = user_fixture()

    conn =
      conn
      |> post(~p"/auth/google/register", %{
        "google_registration" => %{"locale" => "en", "terms_accepted" => "true"}
      })
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=code&state=google-test-state&uid=new-google&email=#{user.email}"
      )

    assert redirected_to(conn) == ~p"/auth/google/complete"
    assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Confirm it once"
    refute get_session(conn, :user_token)
    refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "new-google")

    html = conn |> recycle() |> get(~p"/auth/google/complete") |> html_response(200)
    assert html =~ "This email already has an account"
    assert html =~ "Email me a verification link"
  end

  test "logs in only through an identity already linked to the local account", %{conn: conn} do
    user = user_fixture()

    assert {:ok, _identity} =
             Accounts.link_google_identity(user, %{
               provider_uid: "returning-google",
               email: user.email,
               email_verified: true,
               display_name: user.display_name
             })

    conn =
      conn
      |> post(~p"/auth/google/login")
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=code&state=google-test-state&uid=returning-google&email=#{user.email}"
      )

    assert redirected_to(conn) == ~p"/"
    assert get_session(conn, :user_token)
  end

  test "continues an unlinked Google user into account creation", %{conn: conn} do
    email = unique_user_email()

    conn =
      conn
      |> post(~p"/auth/google/login")
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=code&state=google-test-state&uid=unknown-google&email=#{email}"
      )

    assert redirected_to(conn) == ~p"/auth/google/complete"
    refute Accounts.get_user_by_email(email)
    refute get_session(conn, :user_token)

    html = conn |> recycle() |> get(~p"/auth/google/complete") |> html_response(200)
    assert html =~ "Create your Who Need Help account"
    assert html =~ email
  end

  test "creates and links an unlinked Google user after terms acceptance", %{conn: conn} do
    email = unique_user_email()

    conn =
      conn
      |> post(~p"/auth/google/login")
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=code&state=google-test-state&uid=continue-google&email=#{email}&name=Continued%20Helper"
      )
      |> recycle()
      |> post(~p"/auth/google/complete-registration", %{
        "google_registration" => %{"locale" => "uk", "terms_accepted" => "true"}
      })

    assert redirected_to(conn) == ~p"/"
    assert get_session(conn, :user_token)

    user = Accounts.get_user_by_email(email)
    assert user.confirmed_at
    assert user.accepted_terms_at
    assert user.locale == "uk"
    assert user.display_name == "Continued Helper"

    assert %AuthIdentity{user_id: user_id} =
             Repo.get_by!(AuthIdentity, provider: :google, provider_uid: "continue-google")

    assert user_id == user.id
  end

  test "does not create a continued Google account without accepting terms", %{conn: conn} do
    email = unique_user_email()

    conn =
      conn
      |> post(~p"/auth/google/login")
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=code&state=google-test-state&uid=no-terms-google&email=#{email}"
      )
      |> recycle()
      |> post(~p"/auth/google/complete-registration", %{
        "google_registration" => %{"locale" => "en", "terms_accepted" => "false"}
      })

    assert redirected_to(conn) == ~p"/auth/google/complete"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "18 or older"
    refute Accounts.get_user_by_email(email)
    refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "no-terms-google")
  end

  test "links an existing account after one magic-link verification", %{conn: conn} do
    user = user_fixture()
    assert_email_sent()

    conn =
      conn
      |> post(~p"/auth/google/login")
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=code&state=google-test-state&uid=verified-existing-google&email=#{user.email}"
      )
      |> recycle()
      |> post(~p"/auth/google/verify-existing")

    assert redirected_to(conn) == ~p"/users/log-in"
    assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "finish connecting Google"

    assert_email_sent(fn email ->
      email.subject == "Confirm Google sign-in" and
        email.text_body =~ "connect Google to your Who Need Help account" and
        email.text_body =~ "google_link=" and email.text_body =~ "#token="
    end)

    pending_token = get_session(conn, "pending_google_identity")
    assert is_binary(pending_token)
    {token, _hashed_token} = generate_user_magic_link_token(user)

    conn =
      build_conn()
      |> get(~p"/users/log-in?google_link=#{pending_token}")
      |> recycle()
      |> post(~p"/users/log-in", %{"user" => %{"token" => token}})

    assert redirected_to(conn) == ~p"/"
    assert get_session(conn, :user_token)
    assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Google sign-in connected"

    identity =
      Repo.get_by!(AuthIdentity,
        provider: :google,
        provider_uid: "verified-existing-google"
      )

    assert identity.user_id == user.id

    assert %AuditEvent{actor_id: actor_id, metadata: metadata} =
             Repo.get_by!(AuditEvent,
               action: "auth_identity.connected",
               target_id: identity.id
             )

    assert actor_id == user.id
    assert metadata["method"] == "verified_local_sign_in"
  end

  test "rejects unverified Google email and consumes the flow session", %{conn: conn} do
    conn =
      conn
      |> post(~p"/auth/google/login")
      |> recycle()
      |> get(~p"/auth/google/callback?code=code&state=google-test-state&email_verified=false")

    assert redirected_to(conn) == ~p"/users/log-in"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "verified email"
    assert is_nil(get_session(conn, "google_auth_flow"))
  end

  test "callback cannot be replayed", %{conn: conn} do
    first =
      conn
      |> post(~p"/auth/google/login")
      |> recycle()
      |> get(~p"/auth/google/callback?code=code&state=google-test-state")

    replay =
      first
      |> recycle()
      |> get(~p"/auth/google/callback?code=code&state=google-test-state")

    assert redirected_to(replay) == ~p"/users/log-in"
    assert Phoenix.Flash.get(replay.assigns.flash, :error) =~ "session expired"
  end

  test "links Google only to the authenticated flow owner and records an audit event", %{
    conn: conn
  } do
    user = user_fixture()

    conn =
      conn
      |> log_in_user(user)
      |> post(~p"/auth/google/link")
      |> recycle()
      |> get(
        ~p"/auth/google/callback?code=code&state=google-test-state&uid=linked-google&email=linked@example.com"
      )

    assert redirected_to(conn) == ~p"/users/settings"
    assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "connected"

    identity = Repo.get_by!(AuthIdentity, provider: :google, provider_uid: "linked-google")
    assert identity.user_id == user.id

    assert %AuditEvent{actor_id: actor_id, target_id: target_id} =
             Repo.get_by!(AuditEvent,
               action: "auth_identity.connected",
               target_id: identity.id
             )

    assert actor_id == user.id
    assert target_id == identity.id
  end

  test "rejects a link callback after the authenticated account changes", %{conn: conn} do
    first_user = user_fixture()
    second_user = user_fixture()

    conn =
      conn
      |> log_in_user(first_user)
      |> post(~p"/auth/google/link")
      |> recycle()
      |> log_in_user(second_user)
      |> get(~p"/auth/google/callback?code=code&state=google-test-state&uid=wrong-owner")

    assert redirected_to(conn) == ~p"/users/log-in"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "invalid"
    refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "wrong-owner")
  end

  test "requires authentication to start account linking", %{conn: conn} do
    conn = post(conn, ~p"/auth/google/link")
    assert redirected_to(conn) == ~p"/users/log-in"
  end

  test "rejects a link callback after the sudo window expires", %{conn: conn} do
    user = user_fixture()

    conn =
      conn
      |> log_in_user(user)
      |> post(~p"/auth/google/link")

    token = get_session(conn, :user_token)

    override_token_authenticated_at(
      token,
      DateTime.utc_now(:second) |> DateTime.add(-11, :minute)
    )

    conn =
      conn
      |> recycle()
      |> get(~p"/auth/google/callback?code=code&state=google-test-state&uid=stale-google")

    assert redirected_to(conn) == ~p"/users/log-in"
    refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "stale-google")
  end

  test "disconnects Google in sudo mode and records the audit event", %{conn: conn} do
    user = user_fixture()

    assert {:ok, identity} =
             Accounts.link_google_identity(user, %{
               provider_uid: "disconnect-google",
               email: user.email,
               email_verified: true,
               display_name: user.display_name
             })

    conn =
      conn
      |> log_in_user(user)
      |> delete(~p"/auth/google/link")

    assert redirected_to(conn) == ~p"/users/settings"
    assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "disconnected"
    refute Repo.get(AuthIdentity, identity.id)

    assert %AuditEvent{actor_id: actor_id, target_id: target_id} =
             Repo.get_by!(AuditEvent,
               action: "auth_identity.disconnected",
               target_id: identity.id
             )

    assert actor_id == user.id
    assert target_id == identity.id
  end
end
