defmodule ToweropsWeb.UserSessionControllerTest do
  use ToweropsWeb.ConnCase, async: true

  import Towerops.AccountsFixtures

  alias Towerops.Accounts

  setup do
    # Create users without TOTP for login flow tests (they'll be prompted to enroll)
    %{
      unconfirmed_user: unconfirmed_user_fixture(),
      user: user_fixture(enable_totp: false),
      user_with_totp: user_fixture(enable_totp: true)
    }
  end

  describe "GET /users/log-in" do
    test "renders login page", %{conn: conn} do
      conn = get(conn, ~p"/users/log-in")
      response = html_response(conn, 200)
      assert response =~ "Log in"
      assert response =~ ~p"/users/register"
      assert response =~ "Send me a login link instead"
    end

    test "renders login page with email filled in (sudo mode)", %{conn: conn, user: user} do
      html =
        conn
        |> log_in_user(user)
        |> get(~p"/users/log-in")
        |> html_response(200)

      assert html =~ "You need to reauthenticate"
      refute html =~ "Register"
      assert html =~ "Send me a login link instead"

      assert html =~
               ~s(<input type="email" name="user[email]" id="user_email_password" value="#{user.email}")
    end
  end

  describe "GET /users/log-in/:token" do
    test "logs in confirmed user via magic link", %{conn: conn, user: user} do
      token =
        extract_user_token(fn url ->
          Accounts.deliver_login_instructions(user, url)
        end)

      conn = get(conn, ~p"/users/log-in/#{token}")
      # User redirected to TOTP enrollment (TOTP is mandatory)
      assert redirected_to(conn) == ~p"/account/totp-enrollment"
      assert get_session(conn, :user_token)
      refute Phoenix.Flash.get(conn.assigns.flash, :info)
      assert conn.resp_cookies["_towerops_web_user_remember_me"]
    end

    test "redirects with error for invalid token", %{conn: conn} do
      conn = get(conn, ~p"/users/log-in/invalid-token")
      assert redirected_to(conn) == ~p"/users/log-in"

      assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
               "Magic link is invalid or it has expired."
    end
  end

  describe "POST /users/log-in - email and password" do
    test "logs the user in", %{conn: conn, user: user} do
      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{"email" => user.email, "password" => valid_user_password()}
        })

      assert get_session(conn, :user_token)
      # User redirected to TOTP enrollment (TOTP is mandatory)
      assert redirected_to(conn) == ~p"/account/totp-enrollment"
    end

    test "logs the user in with remember me", %{conn: conn, user: user} do
      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{
            "email" => user.email,
            "password" => valid_user_password(),
            "remember_me" => "true"
          }
        })

      assert conn.resp_cookies["_towerops_web_user_remember_me"]
      # User redirected to TOTP enrollment (TOTP is mandatory)
      assert redirected_to(conn) == ~p"/account/totp-enrollment"
    end

    test "logs the user in with return to", %{conn: conn, user: user} do
      conn =
        conn
        |> init_test_session(user_return_to: "/foo/bar")
        |> post(~p"/users/log-in", %{
          "user" => %{
            "email" => user.email,
            "password" => valid_user_password()
          }
        })

      assert redirected_to(conn) == "/foo/bar"
    end

    test "emits error message with invalid credentials", %{conn: conn, user: user} do
      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{"email" => user.email, "password" => "invalid_password"}
        })

      response = html_response(conn, 200)
      assert response =~ "Log in"
      assert response =~ "Invalid email or password"
    end

    test "redirects to TOTP verification when user has TOTP enabled", %{conn: conn, user_with_totp: user} do
      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{"email" => user.email, "password" => valid_user_password()}
        })

      # Should NOT create session yet
      refute get_session(conn, :user_token)
      # Should store pending auth state
      assert get_session(conn, :pending_totp_user_id) == user.id
      # Should redirect to TOTP verification
      assert redirected_to(conn) == ~p"/users/log-in/totp"
    end

    test "does not create session if password is wrong even for TOTP users", %{
      conn: conn,
      user_with_totp: user
    } do
      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{"email" => user.email, "password" => "wrong_password"}
        })

      response = html_response(conn, 200)
      assert response =~ "Invalid email or password"
      refute get_session(conn, :user_token)
      refute get_session(conn, :pending_totp_user_id)
    end
  end

  describe "GET /users/log-in/totp" do
    test "renders TOTP verification page when pending auth exists", %{conn: conn, user_with_totp: user} do
      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> get(~p"/users/log-in/totp")

      response = html_response(conn, 200)
      assert response =~ "Two-Factor Authentication"
      assert response =~ "Enter the 6-digit code from your authenticator app"
    end

    test "redirects to login when no pending auth", %{conn: conn} do
      conn = get(conn, ~p"/users/log-in/totp")

      assert redirected_to(conn) == ~p"/users/log-in"
      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "Please log in first"
    end
  end

  describe "POST /users/log-in/totp" do
    test "logs in with valid TOTP code", %{conn: conn, user_with_totp: user} do
      # Generate valid TOTP code
      code = NimbleTOTP.verification_code(user.totp_secret)

      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => code}})

      # Should create session
      assert get_session(conn, :user_token)
      # Should clear pending state
      refute get_session(conn, :pending_totp_user_id)
      # Should redirect to orgs (user has no organizations)
      assert redirected_to(conn) == ~p"/orgs"
    end

    test "rejects invalid TOTP code", %{conn: conn, user_with_totp: user} do
      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => "000000"}})

      response = html_response(conn, 200)
      assert response =~ "Invalid authentication code"
      # Should NOT create session
      refute get_session(conn, :user_token)
      # Should keep pending state
      assert get_session(conn, :pending_totp_user_id) == user.id
    end

    test "redirects to login when no pending auth", %{conn: conn} do
      conn = post(conn, ~p"/users/log-in/totp", %{"user" => %{"totp_code" => "123456"}})

      assert redirected_to(conn) == ~p"/users/log-in"
      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "Please log in first"
    end

    test "respects remember_me from initial login", %{conn: conn, user_with_totp: user} do
      code = NimbleTOTP.verification_code(user.totp_secret)

      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id, pending_totp_remember_me: true)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => code}})

      assert conn.resp_cookies["_towerops_web_user_remember_me"]
    end
  end

  describe "POST /users/log-in/totp - recovery codes" do
    test "logs in with valid recovery code", %{conn: conn, user_with_totp: user} do
      # Generate recovery codes
      {:ok, codes} = Accounts.generate_recovery_codes(user.id)
      recovery_code = List.first(codes)

      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => recovery_code}})

      # Should create session
      assert get_session(conn, :user_token)
      # Should clear pending state
      refute get_session(conn, :pending_totp_user_id)
      # Should show warning about recovery code usage
      assert Phoenix.Flash.get(conn.assigns.flash, :warning) =~
               "You used a recovery code"

      assert Phoenix.Flash.get(conn.assigns.flash, :warning) =~
               "Consider regenerating codes"

      assert redirected_to(conn) == ~p"/orgs"
    end

    test "marks recovery code as used after login", %{conn: conn, user_with_totp: user} do
      {:ok, codes} = Accounts.generate_recovery_codes(user.id)
      recovery_code = List.first(codes)

      # Verify code is unused
      assert Accounts.count_unused_recovery_codes(user.id) == 12

      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => recovery_code}})

      assert get_session(conn, :user_token)

      # Verify code was marked as used
      assert Accounts.count_unused_recovery_codes(user.id) == 11
    end

    test "rejects already-used recovery code", %{conn: conn, user_with_totp: user} do
      {:ok, codes} = Accounts.generate_recovery_codes(user.id)
      recovery_code = List.first(codes)

      # Use the code once
      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => recovery_code}})

      assert get_session(conn, :user_token)

      # Try to use it again
      conn =
        build_conn()
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => recovery_code}})

      response = html_response(conn, 200)
      assert response =~ "Invalid authentication code"
      refute get_session(conn, :user_token)
    end

    test "rejects invalid recovery code format", %{conn: conn, user_with_totp: user} do
      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => "INVALID-CODE"}})

      response = html_response(conn, 200)
      assert response =~ "Invalid authentication code"
      refute get_session(conn, :user_token)
    end

    test "accepts both TOTP and recovery codes", %{conn: conn, user_with_totp: user} do
      {:ok, codes} = Accounts.generate_recovery_codes(user.id)
      recovery_code = List.first(codes)

      # First login with TOTP code
      totp_code = NimbleTOTP.verification_code(user.totp_secret)

      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => totp_code}})

      assert get_session(conn, :user_token)
      # No warning for TOTP
      refute Phoenix.Flash.get(conn.assigns.flash, :warning)

      # Second login with recovery code
      conn =
        build_conn()
        |> init_test_session(pending_totp_user_id: user.id)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => recovery_code}})

      assert get_session(conn, :user_token)
      # Warning shown for recovery code
      assert Phoenix.Flash.get(conn.assigns.flash, :warning) =~ "recovery code"
    end

    test "respects remember_me with recovery code", %{conn: conn, user_with_totp: user} do
      {:ok, codes} = Accounts.generate_recovery_codes(user.id)
      recovery_code = List.first(codes)

      conn =
        conn
        |> init_test_session(pending_totp_user_id: user.id, pending_totp_remember_me: true)
        |> post(~p"/users/log-in/totp", %{"user" => %{"totp_code" => recovery_code}})

      assert conn.resp_cookies["_towerops_web_user_remember_me"]
      assert get_session(conn, :user_token)
    end
  end

  describe "POST /users/log-in - magic link" do
    test "sends magic link email when user exists", %{conn: conn, user: user} do
      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{"email" => user.email}
        })

      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
      assert Towerops.Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "login"
    end

    test "logs in confirmed user via magic link token", %{conn: conn, user: user} do
      {token, _hashed_token} = generate_user_magic_link_token(user)

      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{"token" => token}
        })

      assert get_session(conn, :user_token)
      # User redirected to TOTP enrollment (TOTP is mandatory)
      assert redirected_to(conn) == ~p"/account/totp-enrollment"
    end

    test "emits error message when magic link is invalid", %{conn: conn} do
      conn =
        post(conn, ~p"/users/log-in", %{
          "user" => %{"token" => "invalid"}
        })

      assert html_response(conn, 200) =~ "The link is invalid or it has expired."
    end
  end

  describe "DELETE /users/log-out" do
    test "logs the user out", %{conn: conn, user: user} do
      conn = conn |> log_in_user(user) |> delete(~p"/users/log-out")
      assert redirected_to(conn) == ~p"/users/log-in"
      refute get_session(conn, :user_token)
      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
    end

    test "succeeds even if the user is not logged in", %{conn: conn} do
      conn = delete(conn, ~p"/users/log-out")
      assert redirected_to(conn) == ~p"/users/log-in"
      refute get_session(conn, :user_token)
      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
    end
  end
end
