defmodule Aprsme.PacketFieldWhitelistTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  alias Aprsme.PacketFieldWhitelist

  describe "allowed_fields/0" do
    test "returns a list of strings" do
      fields = PacketFieldWhitelist.allowed_fields()
      assert match?([_ | _], fields)
      assert Enum.all?(fields, &is_binary/1)
    end

    test "contains core schema fields" do
      fields = PacketFieldWhitelist.allowed_fields()
      assert "sender" in fields
      assert "base_callsign" in fields
      assert "lat" in fields
      assert "lon" in fields
      assert "received_at" in fields
      assert "raw_packet" in fields
    end
  end

  describe "allowed?/1" do
    test "accepts atoms that match allowed fields" do
      assert PacketFieldWhitelist.allowed?(:sender)
      assert PacketFieldWhitelist.allowed?(:raw_packet)
      assert PacketFieldWhitelist.allowed?(:lat)
    end

    test "accepts strings that match allowed fields" do
      assert PacketFieldWhitelist.allowed?("sender")
      assert PacketFieldWhitelist.allowed?("raw_packet")
    end

    test "rejects unknown atoms" do
      refute PacketFieldWhitelist.allowed?(:totally_bogus)
      refute PacketFieldWhitelist.allowed?(:__struct__)
    end

    test "rejects unknown strings" do
      refute PacketFieldWhitelist.allowed?("id")
      refute PacketFieldWhitelist.allowed?("")
    end

    test "rejects non-atom, non-binary input" do
      refute PacketFieldWhitelist.allowed?(123)
      refute PacketFieldWhitelist.allowed?(nil)
      refute PacketFieldWhitelist.allowed?(%{})
      refute PacketFieldWhitelist.allowed?([])
    end
  end

  describe "filter_fields/1" do
    test "keeps allowed atom keys" do
      input = %{sender: "K5ABC", lat: 30.0, bogus: "x"}
      result = PacketFieldWhitelist.filter_fields(input)
      assert result[:sender] == "K5ABC"
      assert result[:lat] == 30.0
      refute Map.has_key?(result, :bogus)
    end

    test "keeps allowed string keys" do
      input = %{"sender" => "K5ABC", "unknown" => "x"}
      result = PacketFieldWhitelist.filter_fields(input)
      assert result["sender"] == "K5ABC"
      refute Map.has_key?(result, "unknown")
    end

    test "preserves whichever key type caller used" do
      atom_in = %{sender: "a"}
      string_in = %{"sender" => "b"}
      assert PacketFieldWhitelist.filter_fields(atom_in) == %{sender: "a"}
      assert PacketFieldWhitelist.filter_fields(string_in) == %{"sender" => "b"}
    end

    test "returns empty map when no keys are allowed" do
      assert PacketFieldWhitelist.filter_fields(%{bogus: 1, junk: 2}) == %{}
    end

    test "returns empty map on empty input" do
      assert PacketFieldWhitelist.filter_fields(%{}) == %{}
    end
  end

  describe "invalid_fields/1" do
    test "returns only the disallowed keys as strings" do
      input = %{"junk" => 2, sender: "x", bogus: 1}
      assert PacketFieldWhitelist.invalid_fields(input) == ["bogus", "junk"]
    end

    test "returns empty list when every key is allowed" do
      assert PacketFieldWhitelist.invalid_fields(%{sender: "x", lat: 1.0}) == []
    end

    test "returns sorted, deduplicated-looking output" do
      input = %{z_bogus: 1, a_bogus: 2}
      result = PacketFieldWhitelist.invalid_fields(input)
      assert result == Enum.sort(result)
    end
  end

  describe "property: filter_fields/1 is idempotent" do
    property "running filter twice equals running once" do
      check all(map <- map_of(atom(:alphanumeric), term()), max_runs: 25) do
        once = PacketFieldWhitelist.filter_fields(map)
        twice = PacketFieldWhitelist.filter_fields(once)
        assert once == twice
      end
    end

    property "every key in the output satisfies allowed?/1" do
      check all(map <- map_of(atom(:alphanumeric), term()), max_runs: 25) do
        result = PacketFieldWhitelist.filter_fields(map)
        assert Enum.all?(Map.keys(result), &PacketFieldWhitelist.allowed?/1)
      end
    end

    property "invalid_fields and filter_fields partition the input" do
      check all(map <- map_of(atom(:alphanumeric), term()), max_runs: 25) do
        kept = map |> PacketFieldWhitelist.filter_fields() |> Map.keys() |> Enum.map(&to_string/1)
        dropped = PacketFieldWhitelist.invalid_fields(map)
        all_keys = map |> Map.keys() |> Enum.map(&to_string/1)
        assert Enum.sort(kept ++ dropped) == Enum.sort(all_keys)
      end
    end
  end
end
