defmodule Towerops.EctoTypes.IpAddressTest do
  use ExUnit.Case, async: true

  alias Towerops.EctoTypes.IpAddress

  describe "type/0" do
    test "returns :string" do
      assert IpAddress.type() == :string
    end
  end

  describe "cast/1" do
    test "casts nil" do
      assert IpAddress.cast(nil) == {:ok, nil}
    end

    test "casts valid IPv4 addresses" do
      {:ok, ip} = IpAddress.cast("192.168.1.1")
      assert %IpAddress{} = ip
      assert ip.address == "192.168.1.1"
      assert ip.version == :ipv4
      assert ip.tuple == {192, 168, 1, 1}

      # Test other common IPv4 addresses
      assert {:ok, %IpAddress{address: "0.0.0.0"}} = IpAddress.cast("0.0.0.0")
      assert {:ok, %IpAddress{address: "255.255.255.255"}} = IpAddress.cast("255.255.255.255")
      assert {:ok, %IpAddress{address: "127.0.0.1"}} = IpAddress.cast("127.0.0.1")
      assert {:ok, %IpAddress{address: "10.0.0.1"}} = IpAddress.cast("10.0.0.1")
    end

    test "casts valid IPv6 addresses" do
      {:ok, ip} = IpAddress.cast("2001:db8::1")
      assert %IpAddress{} = ip
      assert ip.address == "2001:db8::1"
      assert ip.version == :ipv6
      assert ip.tuple == {8193, 3512, 0, 0, 0, 0, 0, 1}

      # Test other common IPv6 addresses
      assert {:ok, %IpAddress{address: "::1"}} = IpAddress.cast("::1")
      assert {:ok, %IpAddress{address: "::"}} = IpAddress.cast("::")
      assert {:ok, %IpAddress{address: "fe80::1"}} = IpAddress.cast("fe80::1")

      assert {:ok, %IpAddress{address: "2001:0db8:0000:0000:0000:0000:0000:0001"}} =
               IpAddress.cast("2001:0db8:0000:0000:0000:0000:0000:0001")
    end

    test "casts IPv4 tuples" do
      {:ok, ip} = IpAddress.cast({192, 168, 1, 1})
      assert %IpAddress{} = ip
      assert ip.address == "192.168.1.1"
      assert ip.version == :ipv4
      assert ip.tuple == {192, 168, 1, 1}

      assert {:ok, %IpAddress{address: "10.0.0.1"}} = IpAddress.cast({10, 0, 0, 1})
    end

    test "casts IPv6 tuples" do
      {:ok, ip} = IpAddress.cast({8193, 3512, 0, 0, 0, 0, 0, 1})
      assert %IpAddress{} = ip
      assert ip.address == "2001:db8::1"
      assert ip.version == :ipv6
      assert ip.tuple == {8193, 3512, 0, 0, 0, 0, 0, 1}

      assert {:ok, %IpAddress{address: "::1"}} = IpAddress.cast({0, 0, 0, 0, 0, 0, 0, 1})
    end

    test "is idempotent - accepts already cast structs" do
      {:ok, ip} = IpAddress.cast("192.168.1.1")
      assert IpAddress.cast(ip) == {:ok, ip}
    end

    test "strips IPv6 zone IDs before validation" do
      {:ok, ip} = IpAddress.cast("fe80::1%eth0")
      assert ip.address == "fe80::1"
      assert ip.version == :ipv6

      {:ok, ip2} = IpAddress.cast("fe80::1%en0")
      assert ip2.address == "fe80::1"
    end

    test "rejects empty strings" do
      assert IpAddress.cast("") == :error
    end

    test "rejects invalid IP address formats" do
      assert IpAddress.cast("not-an-ip") == :error
      assert IpAddress.cast("999.999.999.999") == :error
      assert IpAddress.cast("192.168.1.1.1") == :error
      assert IpAddress.cast("hello world") == :error
      assert IpAddress.cast("gggg::1") == :error
    end

    test "accepts BSD-style IP notation (fewer than 4 octets)" do
      # :inet.parse_address/1 accepts these formats (BSD compatibility)
      {:ok, ip} = IpAddress.cast("192.168.1")
      assert ip.address == "192.168.1"
      assert ip.tuple == {192, 168, 0, 1}

      {:ok, ip2} = IpAddress.cast("10.1")
      assert ip2.address == "10.1"
      assert ip2.tuple == {10, 0, 0, 1}
    end

    test "rejects CIDR notation" do
      assert IpAddress.cast("192.168.1.0/24") == :error
      assert IpAddress.cast("10.0.0.0/8") == :error
      assert IpAddress.cast("2001:db8::/32") == :error
      assert IpAddress.cast("fe80::/10") == :error
    end

    test "rejects unsupported types" do
      assert IpAddress.cast(12_345) == :error
      assert IpAddress.cast(3.14) == :error
      assert IpAddress.cast(true) == :error
      assert IpAddress.cast([192, 168, 1, 1]) == :error
      assert IpAddress.cast(%{ip: "192.168.1.1"}) == :error
      # 3-tuple, not 4-tuple
      assert IpAddress.cast({192, 168, 1}) == :error
      # 5-tuple
      assert IpAddress.cast({192, 168, 1, 1, 1}) == :error
    end
  end

  describe "load/1" do
    test "loads nil" do
      assert IpAddress.load(nil) == {:ok, nil}
    end

    test "loads valid IPv4 strings from database" do
      {:ok, ip} = IpAddress.load("192.168.1.1")
      assert %IpAddress{} = ip
      assert ip.address == "192.168.1.1"
      assert ip.version == :ipv4
      assert ip.tuple == {192, 168, 1, 1}
    end

    test "loads valid IPv6 strings from database" do
      {:ok, ip} = IpAddress.load("2001:db8::1")
      assert %IpAddress{} = ip
      assert ip.address == "2001:db8::1"
      assert ip.version == :ipv6
    end

    test "returns error for invalid strings" do
      assert IpAddress.load("not-an-ip") == :error
      assert IpAddress.load("192.168.1.0/24") == :error
    end

    test "returns error for non-string types" do
      assert IpAddress.load(12_345) == :error
      assert IpAddress.load({192, 168, 1, 1}) == :error
    end
  end

  describe "dump/1" do
    test "dumps nil" do
      assert IpAddress.dump(nil) == {:ok, nil}
    end

    test "dumps IPv4 struct to string" do
      {:ok, ip} = IpAddress.cast("192.168.1.1")
      assert IpAddress.dump(ip) == {:ok, "192.168.1.1"}
    end

    test "dumps IPv6 struct to string" do
      {:ok, ip} = IpAddress.cast("2001:db8::1")
      assert IpAddress.dump(ip) == {:ok, "2001:db8::1"}
    end

    test "returns error for invalid input" do
      assert IpAddress.dump("192.168.1.1") == :error
      assert IpAddress.dump(12_345) == :error
      assert IpAddress.dump({192, 168, 1, 1}) == :error
      assert IpAddress.dump(%{address: "192.168.1.1"}) == :error
    end
  end

  describe "cast -> dump -> load roundtrip" do
    test "nil roundtrips correctly" do
      {:ok, casted} = IpAddress.cast(nil)
      {:ok, dumped} = IpAddress.dump(casted)
      {:ok, loaded} = IpAddress.load(dumped)
      assert loaded == nil
    end

    test "IPv4 string roundtrips correctly" do
      {:ok, casted} = IpAddress.cast("192.168.1.1")
      {:ok, dumped} = IpAddress.dump(casted)
      {:ok, loaded} = IpAddress.load(dumped)
      assert loaded.address == "192.168.1.1"
      assert loaded.version == :ipv4
      assert loaded.tuple == {192, 168, 1, 1}
    end

    test "IPv6 string roundtrips correctly" do
      {:ok, casted} = IpAddress.cast("2001:db8::1")
      {:ok, dumped} = IpAddress.dump(casted)
      {:ok, loaded} = IpAddress.load(dumped)
      assert loaded.address == "2001:db8::1"
      assert loaded.version == :ipv6
    end

    test "IPv4 tuple roundtrips correctly" do
      {:ok, casted} = IpAddress.cast({10, 0, 0, 1})
      {:ok, dumped} = IpAddress.dump(casted)
      {:ok, loaded} = IpAddress.load(dumped)
      assert loaded.address == "10.0.0.1"
      assert loaded.version == :ipv4
      assert loaded.tuple == {10, 0, 0, 1}
    end

    test "IPv6 tuple roundtrips correctly" do
      {:ok, casted} = IpAddress.cast({0, 0, 0, 0, 0, 0, 0, 1})
      {:ok, dumped} = IpAddress.dump(casted)
      {:ok, loaded} = IpAddress.load(dumped)
      assert loaded.address == "::1"
      assert loaded.version == :ipv6
    end
  end

  describe "new/1" do
    test "creates IPAddress from valid string" do
      {:ok, ip} = IpAddress.new("192.168.1.1")
      assert %IpAddress{} = ip
      assert ip.address == "192.168.1.1"
    end

    test "creates IPAddress from valid tuple" do
      {:ok, ip} = IpAddress.new({192, 168, 1, 1})
      assert %IpAddress{} = ip
      assert ip.address == "192.168.1.1"
    end

    test "returns error for invalid input" do
      assert IpAddress.new("invalid") == :error
      assert IpAddress.new("192.168.1.0/24") == :error
    end
  end

  describe "new!/1" do
    test "creates IPAddress from valid string" do
      ip = IpAddress.new!("192.168.1.1")
      assert %IpAddress{} = ip
      assert ip.address == "192.168.1.1"
    end

    test "creates IPAddress from valid tuple" do
      ip = IpAddress.new!({192, 168, 1, 1})
      assert %IpAddress{} = ip
      assert ip.address == "192.168.1.1"
    end

    test "raises ArgumentError for invalid input" do
      assert_raise ArgumentError, ~r/invalid IP address/, fn ->
        IpAddress.new!("invalid")
      end

      assert_raise ArgumentError, ~r/invalid IP address/, fn ->
        IpAddress.new!("192.168.1.0/24")
      end
    end
  end

  describe "ipv4?/1" do
    test "returns true for IPv4 addresses" do
      ip = IpAddress.new!("192.168.1.1")
      assert IpAddress.ipv4?(ip) == true
    end

    test "returns false for IPv6 addresses" do
      ip = IpAddress.new!("2001:db8::1")
      assert IpAddress.ipv4?(ip) == false
    end

    test "returns false for non-IPAddress structs" do
      assert IpAddress.ipv4?("192.168.1.1") == false
      assert IpAddress.ipv4?(nil) == false
      assert IpAddress.ipv4?(%{version: :ipv4}) == false
    end
  end

  describe "ipv6?/1" do
    test "returns true for IPv6 addresses" do
      ip = IpAddress.new!("2001:db8::1")
      assert IpAddress.ipv6?(ip) == true
    end

    test "returns false for IPv4 addresses" do
      ip = IpAddress.new!("192.168.1.1")
      assert IpAddress.ipv6?(ip) == false
    end

    test "returns false for non-IPAddress structs" do
      assert IpAddress.ipv6?("2001:db8::1") == false
      assert IpAddress.ipv6?(nil) == false
      assert IpAddress.ipv6?(%{version: :ipv6}) == false
    end
  end

  describe "to_string/1" do
    test "converts IPv4 struct to string" do
      ip = IpAddress.new!("192.168.1.1")
      assert IpAddress.to_string(ip) == "192.168.1.1"
    end

    test "converts IPv6 struct to string" do
      ip = IpAddress.new!("2001:db8::1")
      assert IpAddress.to_string(ip) == "2001:db8::1"
    end
  end

  describe "to_tuple/1" do
    test "converts IPv4 struct to tuple" do
      ip = IpAddress.new!("192.168.1.1")
      assert IpAddress.to_tuple(ip) == {192, 168, 1, 1}
    end

    test "converts IPv6 struct to tuple" do
      ip = IpAddress.new!("2001:db8::1")
      assert IpAddress.to_tuple(ip) == {8193, 3512, 0, 0, 0, 0, 0, 1}
    end

    test "converts IPv6 loopback to tuple" do
      ip = IpAddress.new!("::1")
      assert IpAddress.to_tuple(ip) == {0, 0, 0, 0, 0, 0, 0, 1}
    end
  end
end
