defmodule MvWeb.ConnCase do
  @moduledoc """
  This module defines the test case to be used by
  tests that require setting up a connection.

  Such tests rely on `Phoenix.ConnTest` and also
  import other functionality to make it easier
  to build common data structures and query the data layer.

  Finally, if the test case interacts with the database,
  we enable the SQL sandbox, so changes done to the database
  are reverted at the end of every test. If you are using
  PostgreSQL, you can even run database tests asynchronously
  by setting `use MvWeb.ConnCase, async: true`, although
  this option is not recommended for other databases.
  """

  use ExUnit.CaseTemplate

  using do
    quote do
      # The default endpoint for testing
      @endpoint MvWeb.Endpoint

      use MvWeb, :verified_routes

      # Import conveniences for testing with connections
      import Plug.Conn
      import Phoenix.ConnTest
      import MvWeb.ConnCase
    end
  end

  @doc """
  Creates a test user and returns the user struct.
  Accepts attrs to override default values.

  Password handling:
  - If `hashed_password` is provided in attrs, it's used directly
  - If `password` is provided in attrs, it gets hashed automatically
  - If neither is provided, uses default password "password"

  ## Examples

      create_test_user()  # Default user with unique email
      create_test_user(%{email: "custom@example.com"})  # Custom email
      create_test_user(%{password: "secret123"})  # Custom password (gets hashed)
      create_test_user(%{hashed_password: "$2b$..."})  # Pre-hashed password
  """
  def create_test_user(attrs \\ %{}) do
    # Generate unique values to avoid conflicts
    unique_id = System.unique_integer([:positive])

    default_attrs = %{
      email: "user#{unique_id}@example.com",
      oidc_id: "oidc#{unique_id}"
    }

    # Merge provided attrs with defaults
    user_attrs = Map.merge(default_attrs, attrs)

    # Handle password/hashed_password
    final_attrs =
      cond do
        # If hashed_password is already provided, use it as-is
        Map.has_key?(user_attrs, :hashed_password) ->
          user_attrs

        # If password is provided, hash it
        Map.has_key?(user_attrs, :password) ->
          password = Map.get(user_attrs, :password)
          {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password)

          user_attrs
          # Remove plain password
          |> Map.delete(:password)
          |> Map.put(:hashed_password, hashed_password)

        # Neither provided, use default password
        true ->
          password = "password"
          {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password)
          Map.put(user_attrs, :hashed_password, hashed_password)
      end

    Ash.Seed.seed!(Mv.Accounts.User, final_attrs)
  end

  @doc """
  Signs in a user via OIDC for testing by creating a session with the user's token.
  """
  def sign_in_user_via_oidc(conn, user) do
    # Mock OIDC sign-in by creating a token directly
    conn
    |> Phoenix.ConnTest.init_test_session(%{})
    |> AshAuthentication.Plug.Helpers.store_in_session(user)
  end

  @doc """
  Signs in a user via OIDC and returns a connection with the user authenticated.
  By default creates a user with "user@example.com" for consistency.
  The user will have an admin role for authorization.
  """
  def conn_with_oidc_user(conn, user_attrs \\ %{}) do
    # Ensure unique email for OIDC users
    unique_id = System.unique_integer([:positive])

    default_attrs = %{
      email: "oidc.user#{unique_id}@example.com",
      oidc_id: "oidc_#{unique_id}"
    }

    # Create user using Ash.Seed (supports oidc_id)
    user = create_test_user(Map.merge(default_attrs, user_attrs))

    # Create admin role and assign it
    admin_role = Mv.Fixtures.role_fixture("admin")
    system_actor = Mv.Helpers.SystemActor.get_system_actor()

    {:ok, user} =
      user
      |> Ash.Changeset.for_update(:update, %{})
      |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
      |> Ash.update(actor: system_actor)

    # Load role for authorization
    user_with_role = Ash.load!(user, :role, domain: Mv.Accounts, actor: system_actor)

    sign_in_user_via_oidc(conn, user_with_role)
  end

  @doc """
  Signs in a user via password authentication and returns a connection with the user authenticated.
  """
  def conn_with_password_user(conn, user) do
    conn
    |> Phoenix.ConnTest.init_test_session(%{})
    |> AshAuthentication.Plug.Helpers.store_in_session(user)
  end

  @doc """
  Creates a connection with an authenticated user that has an admin role.
  This is useful for tests that need full access to resources.
  """
  def conn_with_admin_user(conn) do
    admin_user = Mv.Fixtures.user_with_role_fixture("admin")
    conn_with_password_user(conn, admin_user)
  end

  setup tags do
    pid = Mv.DataCase.setup_sandbox(tags)

    conn = Phoenix.ConnTest.build_conn()
    # Set metadata for Phoenix.Ecto.SQL.Sandbox plug to allow LiveView processes
    # to share the test's database connection in async tests
    conn = Plug.Conn.put_private(conn, :ecto_sandbox, pid)

    # Handle role tags for future test extensions
    # Default to admin to maintain backward compatibility with existing tests
    role = Map.get(tags, :role, :admin)

    {conn, user} =
      case role do
        :admin ->
          # Create admin user with role for all tests (unless test overrides with its own user)
          # This ensures all tests have an authenticated user with proper authorization
          admin_user = Mv.Fixtures.user_with_role_fixture("admin")
          authenticated_conn = conn_with_password_user(conn, admin_user)
          {authenticated_conn, admin_user}

        :member ->
          # Create member user for role-based testing
          # "member" role uses "own_data" permission set (Mitglied role)
          member_user = Mv.Fixtures.user_with_role_fixture("own_data")
          authenticated_conn = conn_with_password_user(conn, member_user)
          {authenticated_conn, member_user}

        :read_only ->
          # Vorstand/Buchhaltung: can read members, groups; cannot edit or access admin/settings
          read_only_user = Mv.Fixtures.user_with_role_fixture("read_only")
          read_only_user = Mv.Authorization.Actor.ensure_loaded(read_only_user)
          authenticated_conn = conn_with_password_user(conn, read_only_user)
          {authenticated_conn, read_only_user}

        :normal_user ->
          # Kassenwart: can read/update members, groups; cannot access users/settings/admin
          normal_user = Mv.Fixtures.user_with_role_fixture("normal_user")
          authenticated_conn = conn_with_password_user(conn, normal_user)
          {authenticated_conn, normal_user}

        :unauthenticated ->
          # No authentication for unauthenticated tests
          {conn, nil}

        _other ->
          # Fallback: treat unknown role as admin for safety
          admin_user = Mv.Fixtures.user_with_role_fixture("admin")
          authenticated_conn = conn_with_password_user(conn, admin_user)
          {authenticated_conn, admin_user}
      end

    {:ok, conn: conn, current_user: user}
  end
end
