defmodule Towerops.Accounts.UserCredentialTest do
  use Towerops.DataCase

  alias Towerops.Accounts.UserCredential

  describe "changeset/2" do
    test "valid changeset with required fields" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65),
        name: "My Passkey"
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      assert changeset.valid?
      assert get_field(changeset, :name) == "My Passkey"
      assert get_field(changeset, :credential_id) == attrs.credential_id
      assert get_field(changeset, :public_key) == attrs.public_key
    end

    test "requires user_id" do
      attrs = %{
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65),
        name: "My Passkey"
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      refute changeset.valid?
      assert %{user_id: ["can't be blank"]} = errors_on(changeset)
    end

    test "requires credential_id" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        public_key: :crypto.strong_rand_bytes(65),
        name: "My Passkey"
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      refute changeset.valid?
      assert %{credential_id: ["can't be blank"]} = errors_on(changeset)
    end

    test "requires public_key" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        name: "My Passkey"
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      refute changeset.valid?
      assert %{public_key: ["can't be blank"]} = errors_on(changeset)
    end

    test "requires name" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65)
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      refute changeset.valid?
      assert %{name: ["can't be blank"]} = errors_on(changeset)
    end

    test "validates name length minimum" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65),
        name: ""
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      refute changeset.valid?
      # Empty string triggers "can't be blank" before length validation
      assert %{name: ["can't be blank"]} = errors_on(changeset)
    end

    test "validates name length maximum" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65),
        name: String.duplicate("a", 101)
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      refute changeset.valid?
      assert %{name: ["should be at most 100 character(s)"]} = errors_on(changeset)
    end

    test "accepts name at maximum length" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65),
        name: String.duplicate("a", 100)
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      assert changeset.valid?
    end

    test "accepts optional fields" do
      last_used_at = DateTime.truncate(DateTime.utc_now(), :second)

      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65),
        name: "My Passkey",
        sign_count: 42,
        last_used_at: last_used_at,
        aaguid: :crypto.strong_rand_bytes(16),
        transports: ["usb", "nfc"],
        backup_eligible: true,
        backup_state: true,
        attestation_format: "packed"
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      assert changeset.valid?
      assert get_field(changeset, :sign_count) == 42
      assert DateTime.compare(get_field(changeset, :last_used_at), last_used_at) == :eq
      assert get_field(changeset, :aaguid) == attrs.aaguid
      assert get_field(changeset, :transports) == ["usb", "nfc"]
      assert get_field(changeset, :backup_eligible) == true
      assert get_field(changeset, :backup_state) == true
      assert get_field(changeset, :attestation_format) == "packed"
    end

    test "uses default values for optional fields when not provided" do
      attrs = %{
        user_id: Ecto.UUID.generate(),
        credential_id: :crypto.strong_rand_bytes(16),
        public_key: :crypto.strong_rand_bytes(65),
        name: "My Passkey"
      }

      changeset = UserCredential.changeset(%UserCredential{}, attrs)

      assert changeset.valid?
      # These defaults come from the schema definition
      assert get_field(changeset, :sign_count) == 0
      assert get_field(changeset, :transports) == []
      assert get_field(changeset, :backup_eligible) == false
      assert get_field(changeset, :backup_state) == false
    end
  end

  describe "touch_changeset/1" do
    test "updates last_used_at to current time" do
      old_time = DateTime.add(DateTime.utc_now(), -3600, :second)

      credential = %UserCredential{
        sign_count: 5,
        last_used_at: old_time
      }

      changeset = UserCredential.touch_changeset(credential)
      last_used_at = get_field(changeset, :last_used_at)

      # Should be more recent than the old time
      assert DateTime.after?(last_used_at, old_time)
      # Should be within last 2 seconds (account for timing + truncation)
      diff = DateTime.diff(DateTime.utc_now(), last_used_at, :second)
      assert diff <= 2
    end

    test "increments sign_count by 1" do
      credential = %UserCredential{sign_count: 5}

      changeset = UserCredential.touch_changeset(credential)

      assert get_field(changeset, :sign_count) == 6
    end

    test "increments sign_count from 0" do
      credential = %UserCredential{sign_count: 0}

      changeset = UserCredential.touch_changeset(credential)

      assert get_field(changeset, :sign_count) == 1
    end
  end
end
