defmodule MvWeb.OidcPasswordLinkingTest do
  @moduledoc """
  Tests for OIDC account linking when email collision occurs.

  This test suite verifies the security flow when an OIDC login attempts
  to use an email that already exists in the system with a password account.
  """
  use MvWeb.ConnCase, async: true
  require Ash.Query

  setup do
    system_actor = Mv.Helpers.SystemActor.get_system_actor()
    %{actor: system_actor}
  end

  describe "OIDC login with existing email (no oidc_id) - Email Collision" do
    @tag :test_proposal
    test "OIDC register with existing password user email fails with PasswordVerificationRequired",
         %{actor: actor} do
      # Create password-only user
      existing_user =
        create_test_user(%{
          email: "existing@example.com",
          password: "securepassword123",
          oidc_id: nil
        })

      # Try OIDC registration with same email
      user_info = %{
        "sub" => "new_oidc_12345",
        "preferred_username" => "existing@example.com"
      }

      result =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      # Should fail with PasswordVerificationRequired error
      assert {:error, %Ash.Error.Invalid{errors: errors}} = result

      # Check that the error is our custom PasswordVerificationRequired
      password_verification_error =
        Enum.find(errors, fn err ->
          err.__struct__ == Mv.Accounts.User.Errors.PasswordVerificationRequired
        end)

      assert password_verification_error != nil,
             "Should contain PasswordVerificationRequired error"

      assert password_verification_error.user_id == existing_user.id
    end

    @tag :test_proposal
    test "PasswordVerificationRequired error contains necessary context", %{actor: actor} do
      existing_user =
        create_test_user(%{
          email: "test@example.com",
          password: "password123",
          oidc_id: nil
        })

      user_info = %{
        "sub" => "oidc_99999",
        "preferred_username" => "test@example.com"
      }

      {:error, %Ash.Error.Invalid{errors: errors}} =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      password_error =
        Enum.find(errors, fn err ->
          err.__struct__ == Mv.Accounts.User.Errors.PasswordVerificationRequired
        end)

      # Verify error contains all necessary context
      assert password_error.user_id == existing_user.id
      assert password_error.oidc_user_info["sub"] == "oidc_99999"
      assert password_error.oidc_user_info["preferred_username"] == "test@example.com"
    end

    @tag :test_proposal
    test "after successful password verification, oidc_id can be set", %{actor: actor} do
      # Create password user
      user =
        create_test_user(%{
          email: "link@example.com",
          password: "mypassword123",
          oidc_id: nil
        })

      # Simulate password verification passed, now link OIDC
      user_info = %{
        "sub" => "linked_oidc_555",
        "preferred_username" => "link@example.com"
      }

      # Use the link_oidc_id action
      {:ok, updated_user} =
        Mv.Accounts.User
        |> Ash.Query.filter(id == ^user.id)
        |> Ash.read_one!(actor: actor)
        |> Ash.Changeset.for_update(:link_oidc_id, %{
          oidc_id: user_info["sub"],
          oidc_user_info: user_info
        })
        |> Ash.update(actor: actor)

      assert updated_user.id == user.id
      assert updated_user.oidc_id == "linked_oidc_555"
      assert to_string(updated_user.email) == "link@example.com"
      # Password should still exist
      assert updated_user.hashed_password == user.hashed_password
    end

    @tag :test_proposal
    test "password verification with wrong password keeps oidc_id as nil", %{actor: actor} do
      # This test verifies that if password verification fails,
      # the oidc_id should NOT be set

      user =
        create_test_user(%{
          email: "secure@example.com",
          password: "correctpassword",
          oidc_id: nil
        })

      # This test verifies the CONCEPT that wrong password should prevent linking
      # In practice, the password verification happens BEFORE calling link_oidc_id
      # So we just verify that the user still has no oidc_id

      # Attempt to verify with wrong password would fail in the controller/LiveView
      # before link_oidc_id is called, so here we just verify the user state

      # User should still have no oidc_id (no linking happened)
      {:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
      assert is_nil(unchanged_user.oidc_id)
      assert unchanged_user.hashed_password == user.hashed_password
    end
  end

  describe "OIDC login with email of user having different oidc_id - Account Conflict" do
    @tag :test_proposal
    test "OIDC register with email of user having different oidc_id fails", %{actor: actor} do
      # User already linked to OIDC provider A
      _existing_user =
        create_test_user(%{
          email: "linked@example.com",
          oidc_id: "oidc_provider_a_123"
        })

      # Someone tries to register with OIDC provider B using same email
      user_info = %{
        # Different OIDC ID!
        "sub" => "oidc_provider_b_456",
        "preferred_username" => "linked@example.com"
      }

      result =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      # Should fail - cannot link different OIDC account to same email
      assert {:error, %Ash.Error.Invalid{errors: errors}} = result

      # The error should indicate email is already taken
      assert Enum.any?(errors, fn err ->
               (err.__struct__ == Ash.Error.Changes.InvalidAttribute and err.field == :email) or
                 err.__struct__ == Mv.Accounts.User.Errors.PasswordVerificationRequired
             end)
    end

    @tag :test_proposal
    test "existing OIDC user email remains unchanged when oidc_id matches", %{actor: actor} do
      user =
        create_test_user(%{
          email: "oidc@example.com",
          oidc_id: "oidc_stable_789"
        })

      # Same OIDC ID, same email - should just sign in
      user_info = %{
        "sub" => "oidc_stable_789",
        "preferred_username" => "oidc@example.com"
      }

      # This should work via upsert
      {:ok, updated_user} =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      assert updated_user.id == user.id
      assert updated_user.oidc_id == "oidc_stable_789"
      assert to_string(updated_user.email) == "oidc@example.com"
    end
  end

  describe "Email update during OIDC linking" do
    @tag :test_proposal
    test "linking OIDC to password account updates email if different in OIDC", %{actor: actor} do
      # Password user with old email
      user =
        create_test_user(%{
          email: "oldemail@example.com",
          password: "password123",
          oidc_id: nil
        })

      # OIDC provider returns new email (user changed it there)
      user_info = %{
        "sub" => "oidc_link_999",
        "preferred_username" => "newemail@example.com"
      }

      # After password verification, link and update email
      {:ok, updated_user} =
        Mv.Accounts.User
        |> Ash.Query.filter(id == ^user.id)
        |> Ash.read_one!(actor: actor)
        |> Ash.Changeset.for_update(:link_oidc_id, %{
          oidc_id: user_info["sub"],
          oidc_user_info: user_info
        })
        |> Ash.update(actor: actor)

      assert updated_user.oidc_id == "oidc_link_999"
      assert to_string(updated_user.email) == "newemail@example.com"
    end

    @tag :test_proposal
    test "email change during linking triggers member email sync", %{actor: actor} do
      # Create member
      member =
        Ash.Seed.seed!(Mv.Membership.Member, %{
          email: "member@example.com",
          first_name: "Test",
          last_name: "User"
        })

      # Create user linked to member
      user =
        Ash.Seed.seed!(Mv.Accounts.User, %{
          email: "member@example.com",
          hashed_password: "dummy_hash",
          oidc_id: nil,
          member_id: member.id
        })

      # Link OIDC with new email
      user_info = %{
        "sub" => "oidc_sync_777",
        "preferred_username" => "newemail@example.com"
      }

      {:ok, updated_user} =
        Mv.Accounts.User
        |> Ash.Query.filter(id == ^user.id)
        |> Ash.read_one!(actor: actor)
        |> Ash.Changeset.for_update(:link_oidc_id, %{
          oidc_id: user_info["sub"],
          oidc_user_info: user_info
        })
        |> Ash.update(actor: actor)

      # Verify user email changed
      assert to_string(updated_user.email) == "newemail@example.com"

      # Verify member email was synced
      {:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
      assert to_string(updated_member.email) == "newemail@example.com"
    end
  end

  describe "Edge cases" do
    @tag :test_proposal
    test "user with empty string oidc_id is treated as password-only user", %{actor: actor} do
      _user =
        create_test_user(%{
          email: "empty@example.com",
          password: "password123",
          oidc_id: ""
        })

      # Try OIDC registration with same email
      user_info = %{
        "sub" => "oidc_new_111",
        "preferred_username" => "empty@example.com"
      }

      result =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      # Should trigger PasswordVerificationRequired (empty string = no OIDC)
      assert {:error, %Ash.Error.Invalid{errors: errors}} = result

      password_error =
        Enum.find(errors, fn err ->
          err.__struct__ == Mv.Accounts.User.Errors.PasswordVerificationRequired
        end)

      assert password_error != nil
    end

    @tag :test_proposal
    test "cannot link same oidc_id to multiple users", %{actor: actor} do
      # User 1 with OIDC
      _user1 =
        create_test_user(%{
          email: "user1@example.com",
          oidc_id: "shared_oidc_333"
        })

      # Try to create user 2 with same OIDC ID using raw Ash.Changeset
      # (create_test_user uses Ash.Seed which does upsert)
      result =
        Mv.Accounts.User
        |> Ash.Changeset.for_create(:create_user, %{
          email: "user2@example.com"
        })
        |> Ash.Changeset.force_change_attribute(:oidc_id, "shared_oidc_333")
        |> Ash.create(actor: actor)

      # Should fail due to unique constraint on oidc_id
      assert match?({:error, %Ash.Error.Invalid{}}, result)

      {:error, error} = result
      # Verify the error is about oidc_id uniqueness
      assert Enum.any?(error.errors, fn err ->
               match?(%Ash.Error.Changes.InvalidAttribute{field: :oidc_id}, err)
             end)
    end
  end

  describe "OIDC login with passwordless user - Requires Linking Flow" do
    test "user without password and without oidc_id triggers PasswordVerificationRequired", %{
      actor: actor
    } do
      # Create user without password (e.g., invited user)
      {:ok, existing_user} =
        Mv.Accounts.User
        |> Ash.Changeset.for_create(:create_user, %{
          email: "invited@example.com"
        })
        |> Ash.create(actor: actor)

      # Verify user has no password and no oidc_id
      assert is_nil(existing_user.hashed_password)
      assert is_nil(existing_user.oidc_id)

      # OIDC registration should trigger linking flow (not automatic)
      user_info = %{
        "sub" => "auto_link_oidc_123",
        "preferred_username" => "invited@example.com"
      }

      result =
        Mv.Accounts.create_register_with_rauthy(%{
          user_info: user_info,
          oauth_tokens: %{"access_token" => "test_token"}
        })

      # Should fail with PasswordVerificationRequired
      # The LinkOidcAccountLive will auto-link without password prompt
      assert {:error, %Ash.Error.Invalid{}} = result
      {:error, error} = result

      assert Enum.any?(error.errors, fn err ->
               match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
             end)
    end

    test "user without password but WITH password later requires verification", %{actor: actor} do
      # Create user without password first
      {:ok, user} =
        Mv.Accounts.User
        |> Ash.Changeset.for_create(:create_user, %{
          email: "added-password@example.com"
        })
        |> Ash.create(actor: actor)

      # User sets password later (using admin action)
      {:ok, user_with_password} =
        user
        |> Ash.Changeset.for_update(:admin_set_password, %{
          password: "newpassword123"
        })
        |> Ash.update(actor: actor)

      assert not is_nil(user_with_password.hashed_password)

      # Now OIDC login should require password verification
      user_info = %{
        "sub" => "needs_verification",
        "preferred_username" => "added-password@example.com"
      }

      result =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{"access_token" => "test_token"}
          },
          actor: actor
        )

      # Should fail with PasswordVerificationRequired
      assert {:error, %Ash.Error.Invalid{}} = result
      {:error, error} = result

      assert Enum.any?(error.errors, fn err ->
               match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
             end)
    end
  end

  describe "OIDC login with different oidc_id - Hard Error" do
    test "user with different oidc_id cannot be linked (hard error)", %{actor: actor} do
      # Create user with existing OIDC ID
      {:ok, existing_user} =
        Mv.Accounts.User
        |> Ash.Changeset.for_create(:create_user, %{
          email: "already-linked@example.com"
        })
        |> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999")
        |> Ash.create(actor: actor)

      assert existing_user.oidc_id == "original_oidc_999"

      # Try to register with same email but different OIDC ID
      user_info = %{
        "sub" => "different_oidc_888",
        "preferred_username" => "already-linked@example.com"
      }

      result =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{"access_token" => "test_token"}
          },
          actor: actor
        )

      # Should fail with hard error (not PasswordVerificationRequired)
      assert {:error, %Ash.Error.Invalid{}} = result
      {:error, error} = result

      # Should NOT be PasswordVerificationRequired
      refute Enum.any?(error.errors, fn err ->
               match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
             end)

      # Should be a validation error about email already linked
      assert Enum.any?(error.errors, fn err ->
               case err do
                 %Ash.Error.Changes.InvalidAttribute{message: msg} ->
                   String.contains?(msg, "already linked to a different OIDC account")

                 _ ->
                   false
               end
             end)
    end

    test "cannot link different oidc_id even with password verification", %{actor: actor} do
      # Create user with password AND existing OIDC ID
      existing_user =
        create_test_user(%{
          email: "password-and-oidc@example.com",
          password: "mypassword123",
          oidc_id: "first_oidc_111"
        })

      assert existing_user.oidc_id == "first_oidc_111"
      assert not is_nil(existing_user.hashed_password)

      # Try to register with different OIDC ID
      user_info = %{
        "sub" => "second_oidc_222",
        "preferred_username" => "password-and-oidc@example.com"
      }

      result =
        Mv.Accounts.create_register_with_rauthy(
          %{
            user_info: user_info,
            oauth_tokens: %{"access_token" => "test_token"}
          },
          actor: actor
        )

      # Should fail - cannot link different OIDC ID
      assert {:error, %Ash.Error.Invalid{}} = result
      {:error, error} = result

      # Should be a hard error, not password verification
      refute Enum.any?(error.errors, fn err ->
               match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
             end)
    end
  end
end
