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

  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 "does not automatically merge an existing email account", %{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"/users/log-in"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "already exists"
    refute get_session(conn, :user_token)
    refute Repo.get_by(AuthIdentity, provider: :google, provider_uid: "new-google")
  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 "does not create an account from the login-only flow", %{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"/users/log-in"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "not connected"
    refute Accounts.get_user_by_email(email)
    refute get_session(conn, :user_token)
  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
