defmodule Aprsme.Accounts.UserTokenTest do
  use Aprsme.DataCase, async: true

  import Aprsme.AccountsFixtures

  alias Aprsme.Accounts.UserToken
  alias Aprsme.Repo

  describe "build_session_token/1" do
    test "returns a binary token and a UserToken struct with context 'session'" do
      user = user_fixture()
      {token, user_token} = UserToken.build_session_token(user)

      assert byte_size(token) == 32
      assert user_token.token == token
      assert user_token.context == "session"
      assert user_token.user_id == user.id
      assert is_nil(user_token.sent_to)
    end

    test "produces unique tokens each call" do
      user = user_fixture()
      {t1, _} = UserToken.build_session_token(user)
      {t2, _} = UserToken.build_session_token(user)
      refute t1 == t2
    end
  end

  describe "verify_session_token_query/1" do
    test "returns the user when token is valid" do
      user = user_fixture()
      {token, user_token} = UserToken.build_session_token(user)
      Repo.insert!(user_token)
      {:ok, query} = UserToken.verify_session_token_query(token)
      loaded = Repo.one(query)
      assert loaded.id == user.id
    end

    test "returns nil for an unknown token" do
      {:ok, query} = UserToken.verify_session_token_query(:crypto.strong_rand_bytes(32))
      assert Repo.one(query) == nil
    end
  end

  describe "build_email_token/2" do
    test "returns a URL-safe base64 token and a hashed UserToken" do
      user = user_fixture()
      {encoded, user_token} = UserToken.build_email_token(user, "confirm")

      assert {:ok, _} = Base.url_decode64(encoded, padding: false)
      assert user_token.context == "confirm"
      assert user_token.sent_to == user.email
      assert user_token.user_id == user.id

      # The stored token is a SHA-256 digest (32 bytes), not the encoded plaintext.
      assert byte_size(user_token.token) == 32
      refute user_token.token == encoded
    end

    test "supports reset_password context" do
      user = user_fixture()
      {_token, user_token} = UserToken.build_email_token(user, "reset_password")
      assert user_token.context == "reset_password"
    end
  end

  describe "verify_email_token_query/2" do
    test "returns :error for malformed tokens" do
      assert UserToken.verify_email_token_query("not-base64!", "confirm") == :error
    end

    test "returns {:ok, query} that finds the user for a valid token" do
      user = user_fixture()
      {encoded, user_token} = UserToken.build_email_token(user, "confirm")
      Repo.insert!(user_token)

      {:ok, query} = UserToken.verify_email_token_query(encoded, "confirm")
      assert Repo.one(query).id == user.id
    end

    test "returns a query matching no user for a valid-looking but unknown token" do
      # Randomly-generated encoded token that doesn't exist in the DB.
      encoded = 32 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)
      {:ok, query} = UserToken.verify_email_token_query(encoded, "confirm")
      assert Repo.one(query) == nil
    end
  end

  describe "verify_change_email_token_query/2" do
    test "returns :error for malformed tokens" do
      assert UserToken.verify_change_email_token_query("not-base64!", "change:new@example.com") == :error
    end

    test "accepts a well-formed token for a change: context" do
      user = user_fixture()
      # Reuse build_email_token/2 (same hashing) for a realistic token.
      {encoded, user_token} = UserToken.build_email_token(user, "change:new@example.com")
      Repo.insert!(user_token)

      {:ok, query} = UserToken.verify_change_email_token_query(encoded, "change:new@example.com")
      # verify_change_email_token_query doesn't select :user, it selects tokens.
      token_row = Repo.one(query)
      assert %_{context: "change:new@example.com"} = token_row
    end
  end

  describe "token_and_context_query/2" do
    test "returns a query scoped by token and context" do
      user = user_fixture()
      {token, user_token} = UserToken.build_session_token(user)
      Repo.insert!(user_token)

      query = UserToken.token_and_context_query(token, "session")
      found = Repo.one(query)
      assert found.token == token
      assert found.context == "session"
    end

    test "returns nothing for mismatched context" do
      user = user_fixture()
      {token, user_token} = UserToken.build_session_token(user)
      Repo.insert!(user_token)

      assert token |> UserToken.token_and_context_query("confirm") |> Repo.one() == nil
    end
  end

  describe "user_and_contexts_query/2" do
    setup do
      user = user_fixture()
      {_token, session_token} = UserToken.build_session_token(user)
      {_token, confirm_token} = UserToken.build_email_token(user, "confirm")
      Repo.insert!(session_token)
      Repo.insert!(confirm_token)
      {:ok, user: user}
    end

    test "with :all returns every token for the user", %{user: user} do
      tokens = user |> UserToken.user_and_contexts_query(:all) |> Repo.all()
      assert length(tokens) == 2
      contexts = tokens |> Enum.map(& &1.context) |> Enum.sort()
      assert contexts == ["confirm", "session"]
    end

    test "with a specific context list filters", %{user: user} do
      tokens = user |> UserToken.user_and_contexts_query(["session"]) |> Repo.all()
      assert length(tokens) == 1
      assert hd(tokens).context == "session"
    end

    test "with an unmatched context returns empty", %{user: user} do
      assert user |> UserToken.user_and_contexts_query(["reset_password"]) |> Repo.all() == []
    end
  end
end
