defmodule MvWeb.OidcE2EFlowTest do
  @moduledoc """
  End-to-end tests for OIDC authentication flows.

  These tests simulate the complete user journey through OIDC authentication,
  including account linking scenarios.
  """
  use MvWeb.ConnCase, async: true
  require Ash.Query

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

  describe "E2E: New OIDC user registration" do
    test "new user can register via OIDC", %{conn: _conn, actor: actor} do
      # Simulate OIDC callback for brand new user
      user_info = %{
        "sub" => "new_oidc_user_123",
        "preferred_username" => "newuser@example.com"
      }

      # Call register action
      result =
        Mv.Accounts.create_register_with_oidc(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      assert {:ok, new_user} = result
      assert to_string(new_user.email) == "newuser@example.com"
      assert new_user.oidc_id == "new_oidc_user_123"
      assert is_nil(new_user.hashed_password)

      # Verify user can be found by oidc_id
      result =
        Mv.Accounts.read_sign_in_with_oidc(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      found_user =
        case result do
          {:ok, u} when is_struct(u) -> u
          {:ok, [u]} -> u
          _ -> flunk("Expected user, got: #{inspect(result)}")
        end

      assert found_user.id == new_user.id
    end
  end

  describe "E2E: Existing OIDC user sign-in" do
    test "existing OIDC user can sign in and email updates", %{conn: _conn, actor: actor} do
      # Create OIDC user
      user =
        create_test_user(%{
          email: "oldmail@example.com",
          oidc_id: "oidc_existing_999"
        })

      # User changed email at OIDC provider
      updated_user_info = %{
        "sub" => "oidc_existing_999",
        "preferred_username" => "newmail@example.com"
      }

      # Register (upsert) with new email
      {:ok, updated_user} =
        Mv.Accounts.create_register_with_oidc(
          %{
            user_info: updated_user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      # Same user, updated email
      assert updated_user.id == user.id
      assert to_string(updated_user.email) == "newmail@example.com"
      assert updated_user.oidc_id == "oidc_existing_999"
    end
  end

  describe "E2E: OIDC with existing password account (Email Collision)" do
    test "OIDC registration with password account email triggers PasswordVerificationRequired",
         %{conn: _conn, actor: actor} do
      # Step 1: Create a password-only user
      password_user =
        create_test_user(%{
          email: "collision@example.com",
          password: "mypassword123",
          oidc_id: nil
        })

      # Step 2: Try to register via OIDC with same email
      user_info = %{
        "sub" => "oidc_new_777",
        "preferred_username" => "collision@example.com"
      }

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

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

      password_error =
        Enum.find(errors, fn err ->
          match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
        end)

      assert password_error != nil
      assert password_error.user_id == password_user.id
      assert password_error.oidc_user_info["sub"] == "oidc_new_777"
      assert password_error.oidc_user_info["preferred_username"] == "collision@example.com"
    end

    test "full E2E flow: OIDC collision -> password verification -> account linked",
         %{conn: _conn, actor: actor} do
      # Step 1: Create password user
      password_user =
        create_test_user(%{
          email: "full@example.com",
          password: "testpass123",
          oidc_id: nil
        })

      # Step 2: OIDC registration triggers error
      user_info = %{
        "sub" => "oidc_link_888",
        "preferred_username" => "full@example.com"
      }

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

      # Extract the error
      password_error =
        Enum.find(errors, fn err ->
          match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
        end)

      assert password_error != nil

      # Step 3: User verifies password (this would happen in LiveView)
      # Here we simulate successful password verification

      # Step 4: Link OIDC account after verification
      {:ok, linked_user} =
        Mv.Accounts.User
        |> Ash.Query.filter(id == ^password_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 account is now linked
      assert linked_user.id == password_user.id
      assert linked_user.oidc_id == "oidc_link_888"
      assert to_string(linked_user.email) == "full@example.com"
      # Password should still exist
      assert linked_user.hashed_password == password_user.hashed_password

      # Step 5: User can now sign in via OIDC
      result =
        Mv.Accounts.read_sign_in_with_oidc(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      signed_in_user =
        case result do
          {:ok, u} when is_struct(u) -> u
          {:ok, [u]} -> u
          _ -> flunk("Expected user, got: #{inspect(result)}")
        end

      assert signed_in_user.id == password_user.id
      assert signed_in_user.oidc_id == "oidc_link_888"
    end

    test "E2E: OIDC collision with different email at provider updates email after linking",
         %{conn: _conn, actor: actor} do
      # Password user with old email
      password_user =
        create_test_user(%{
          email: "old@example.com",
          password: "pass123",
          oidc_id: nil
        })

      # OIDC provider has new email
      user_info = %{
        "sub" => "oidc_new_email_555",
        "preferred_username" => "old@example.com"
      }

      # Collision detected
      {:error, %Ash.Error.Invalid{}} =
        Mv.Accounts.create_register_with_oidc(%{
          user_info: user_info,
          oauth_tokens: %{}
        })

      # After password verification, link with OIDC info that has NEW email
      updated_user_info = %{
        "sub" => "oidc_new_email_555",
        "preferred_username" => "new@example.com"
      }

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

      # Email should be updated to match OIDC provider
      assert to_string(linked_user.email) == "new@example.com"
      assert linked_user.oidc_id == "oidc_new_email_555"
    end
  end

  describe "E2E: OIDC with linked member" do
    test "E2E: email sync to member when linking OIDC to password account", %{
      conn: _conn,
      actor: actor
    } do
      # Create member
      member =
        Ash.Seed.seed!(Mv.Membership.Member, %{
          email: "member@example.com",
          first_name: "Test",
          last_name: "User"
        })

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

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

      # Collision detected
      {:error, %Ash.Error.Invalid{}} =
        Mv.Accounts.create_register_with_oidc(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      # After password verification, link OIDC with NEW email
      updated_user_info = %{
        "sub" => "oidc_member_333",
        "preferred_username" => "newmember@example.com"
      }

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

      # User email updated
      assert to_string(linked_user.email) == "newmember@example.com"

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

  describe "E2E: Security scenarios" do
    test "E2E: password-only user cannot be accessed via OIDC without password", %{
      conn: _conn,
      actor: actor
    } do
      # Create password user
      _password_user =
        create_test_user(%{
          email: "secure@example.com",
          password: "securepass123",
          oidc_id: nil
        })

      # Attacker tries to sign in via OIDC with same email
      user_info = %{
        "sub" => "attacker_oidc_666",
        "preferred_username" => "secure@example.com"
      }

      # Sign-in should fail (no matching oidc_id)
      result =
        Mv.Accounts.read_sign_in_with_oidc(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

      case result do
        {:ok, []} ->
          :ok

        {:ok, nil} ->
          :ok

        {:error, %Ash.Error.Forbidden{}} ->
          :ok

        other ->
          flunk("Expected no access, got: #{inspect(other)}")
      end

      # Registration should trigger password requirement
      {:error, %Ash.Error.Invalid{errors: errors}} =
        Mv.Accounts.create_register_with_oidc(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

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

    test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{
      conn: _conn,
      actor: actor
    } do
      # User linked to OIDC provider A
      _user =
        create_test_user(%{
          email: "linked@example.com",
          oidc_id: "provider_a_123"
        })

      # Attacker tries to register with OIDC provider B using same email
      user_info = %{
        "sub" => "provider_b_456",
        "preferred_username" => "linked@example.com"
      }

      # Should trigger hard error (not PasswordVerificationRequired)
      {:error, %Ash.Error.Invalid{errors: errors}} =
        Mv.Accounts.create_register_with_oidc(
          %{
            user_info: user_info,
            oauth_tokens: %{}
          },
          actor: actor
        )

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

               _ ->
                 false
             end)

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

    test "E2E: empty string oidc_id is treated as password-only account", %{
      conn: _conn,
      actor: actor
    } do
      # User with empty oidc_id
      _password_user =
        create_test_user(%{
          email: "empty@example.com",
          password: "pass123",
          oidc_id: ""
        })

      # Try OIDC registration
      user_info = %{
        "sub" => "oidc_new_222",
        "preferred_username" => "empty@example.com"
      }

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

      # Should require password (empty string = no OIDC)
      assert Enum.any?(errors, fn err ->
               match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err)
             end)
    end
  end

  describe "E2E: Error scenarios" do
    test "E2E: OIDC registration without oidc_id fails", %{conn: _conn, actor: actor} do
      user_info = %{
        "preferred_username" => "noid@example.com"
      }

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

      assert Enum.any?(errors, fn err ->
               match?(%Ash.Error.Changes.InvalidChanges{}, err)
             end)
    end

    test "E2E: OIDC registration without email fails", %{conn: _conn, actor: actor} do
      user_info = %{
        "sub" => "noemail_123"
      }

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

      assert Enum.any?(errors, fn err ->
               match?(%Ash.Error.Changes.Required{field: :email}, err)
             end)
    end
  end
end
