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

  alias Towerops.EctoTypes.SnmpOid

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

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

    test "casts valid numeric OID with leading dot" do
      {:ok, oid} = SnmpOid.cast(".1.3.6.1.2.1.1.1.0")
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
      assert oid.parts == [1, 3, 6, 1, 2, 1, 1, 1, 0]
      assert oid.named == nil
    end

    test "casts valid numeric OID without leading dot" do
      {:ok, oid} = SnmpOid.cast("1.3.6.1.2.1.1.1.0")
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
      assert oid.parts == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end

    test "casts OID from integer list" do
      {:ok, oid} = SnmpOid.cast([1, 3, 6, 1, 2, 1, 1, 1, 0])
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
      assert oid.parts == [1, 3, 6, 1, 2, 1, 1, 1, 0]
      assert oid.named == nil
    end

    test "casts single-part OIDs" do
      {:ok, oid} = SnmpOid.cast(".1")
      assert oid.oid == ".1"
      assert oid.parts == [1]
    end

    test "casts OIDs with large numbers" do
      {:ok, oid} = SnmpOid.cast(".1.3.6.1.4.1.9999.1.2.3")
      assert oid.oid == ".1.3.6.1.4.1.9999.1.2.3"
      assert oid.parts == [1, 3, 6, 1, 4, 1, 9999, 1, 2, 3]
    end

    test "is idempotent - accepts already cast structs" do
      {:ok, oid} = SnmpOid.cast(".1.3.6.1.2.1.1.1.0")
      assert SnmpOid.cast(oid) == {:ok, oid}
    end

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

    test "rejects invalid OID formats" do
      assert SnmpOid.cast("not-an-oid") == :error
      assert SnmpOid.cast(".1..3.6") == :error
      assert SnmpOid.cast("..1.3.6") == :error
      assert SnmpOid.cast(".1.3.6.") == :error
      assert SnmpOid.cast(".1.3.-6") == :error
      assert SnmpOid.cast(".abc.def") == :error
    end

    test "rejects list with negative integers" do
      assert SnmpOid.cast([1, 3, -6, 1]) == :error
    end

    test "rejects list with non-integers" do
      assert SnmpOid.cast([1, 3, "6", 1]) == :error
      assert SnmpOid.cast([1.5, 3.2]) == :error
    end

    test "rejects unsupported types" do
      assert SnmpOid.cast(12_345) == :error
      assert SnmpOid.cast(3.14) == :error
      assert SnmpOid.cast(true) == :error
      assert SnmpOid.cast(%{oid: ".1.3.6.1"}) == :error
    end
  end

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

    test "loads valid OID strings from database" do
      {:ok, oid} = SnmpOid.load(".1.3.6.1.2.1.1.1.0")
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
      assert oid.parts == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end

    test "loads OID without leading dot" do
      {:ok, oid} = SnmpOid.load("1.3.6.1.2.1.1.1.0")
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
    end

    test "returns error for invalid strings" do
      assert SnmpOid.load("not-an-oid") == :error
      assert SnmpOid.load(".1..3.6") == :error
    end

    test "returns error for non-string types" do
      assert SnmpOid.load(12_345) == :error
      assert SnmpOid.load([1, 3, 6, 1]) == :error
    end
  end

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

    test "dumps OID struct to dotted-decimal string with leading dot" do
      {:ok, oid} = SnmpOid.cast("1.3.6.1.2.1.1.1.0")
      assert SnmpOid.dump(oid) == {:ok, ".1.3.6.1.2.1.1.1.0"}
    end

    test "returns error for invalid input" do
      assert SnmpOid.dump(".1.3.6.1.2.1.1.1.0") == :error
      assert SnmpOid.dump(12_345) == :error
      assert SnmpOid.dump([1, 3, 6, 1]) == :error
      assert SnmpOid.dump(%{oid: ".1.3.6.1"}) == :error
    end
  end

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

    test "numeric OID string roundtrips correctly" do
      {:ok, casted} = SnmpOid.cast(".1.3.6.1.2.1.1.1.0")
      {:ok, dumped} = SnmpOid.dump(casted)
      {:ok, loaded} = SnmpOid.load(dumped)
      assert loaded.oid == ".1.3.6.1.2.1.1.1.0"
      assert loaded.parts == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end

    test "OID without leading dot roundtrips with leading dot added" do
      {:ok, casted} = SnmpOid.cast("1.3.6.1.2.1.1.1.0")
      {:ok, dumped} = SnmpOid.dump(casted)
      {:ok, loaded} = SnmpOid.load(dumped)
      assert loaded.oid == ".1.3.6.1.2.1.1.1.0"
    end

    test "integer list roundtrips correctly" do
      {:ok, casted} = SnmpOid.cast([1, 3, 6, 1, 2, 1, 1, 1, 0])
      {:ok, dumped} = SnmpOid.dump(casted)
      {:ok, loaded} = SnmpOid.load(dumped)
      assert loaded.oid == ".1.3.6.1.2.1.1.1.0"
      assert loaded.parts == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end
  end

  describe "new/1" do
    test "creates SnmpOid from valid string" do
      {:ok, oid} = SnmpOid.new(".1.3.6.1.2.1.1.1.0")
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
    end

    test "creates SnmpOid from valid list" do
      {:ok, oid} = SnmpOid.new([1, 3, 6, 1, 2, 1, 1, 1, 0])
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
    end

    test "returns error for invalid input" do
      assert SnmpOid.new("invalid") == :error
      assert SnmpOid.new(".1..3.6") == :error
    end
  end

  describe "new!/1" do
    test "creates SnmpOid from valid string" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1.1.0")
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
    end

    test "creates SnmpOid from valid list" do
      oid = SnmpOid.new!([1, 3, 6, 1, 2, 1, 1, 1, 0])
      assert %SnmpOid{} = oid
      assert oid.oid == ".1.3.6.1.2.1.1.1.0"
    end

    test "raises ArgumentError for invalid input" do
      assert_raise ArgumentError, ~r/invalid SNMP OID/, fn ->
        SnmpOid.new!("invalid")
      end

      assert_raise ArgumentError, ~r/invalid SNMP OID/, fn ->
        SnmpOid.new!(".1..3.6")
      end
    end
  end

  describe "to_string/1" do
    test "converts OID struct to dotted-decimal string" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1.1.0")
      assert SnmpOid.to_string(oid) == ".1.3.6.1.2.1.1.1.0"
    end

    test "preserves leading dot" do
      oid = SnmpOid.new!("1.3.6.1.2.1.1.1.0")
      assert SnmpOid.to_string(oid) == ".1.3.6.1.2.1.1.1.0"
    end
  end

  describe "to_list/1" do
    test "converts OID struct to integer list" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1.1.0")
      assert SnmpOid.to_list(oid) == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end

    test "handles single-part OIDs" do
      oid = SnmpOid.new!(".1")
      assert SnmpOid.to_list(oid) == [1]
    end
  end

  describe "parent/1" do
    test "returns parent OID by removing last part" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1.1.0")
      parent = SnmpOid.parent(oid)
      assert parent.oid == ".1.3.6.1.2.1.1.1"
      assert parent.parts == [1, 3, 6, 1, 2, 1, 1, 1]
    end

    test "returns same OID for single-part OIDs" do
      oid = SnmpOid.new!(".1")
      parent = SnmpOid.parent(oid)
      assert parent.oid == ".1"
    end

    test "can get grandparent by calling parent twice" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1.1.0")
      grandparent = oid |> SnmpOid.parent() |> SnmpOid.parent()
      assert grandparent.oid == ".1.3.6.1.2.1.1"
    end
  end

  describe "child_of?/2" do
    test "returns true for direct child" do
      parent = SnmpOid.new!(".1.3.6.1.2.1.1")
      child = SnmpOid.new!(".1.3.6.1.2.1.1.1")
      assert SnmpOid.child_of?(child, parent) == true
    end

    test "returns true for nested child" do
      parent = SnmpOid.new!(".1.3.6.1.2.1")
      child = SnmpOid.new!(".1.3.6.1.2.1.1.1.0")
      assert SnmpOid.child_of?(child, parent) == true
    end

    test "returns false for parent of child" do
      parent = SnmpOid.new!(".1.3.6.1.2.1.1")
      child = SnmpOid.new!(".1.3.6.1.2.1.1.1")
      assert SnmpOid.child_of?(parent, child) == false
    end

    test "returns false for sibling OIDs" do
      oid1 = SnmpOid.new!(".1.3.6.1.2.1.1.1")
      oid2 = SnmpOid.new!(".1.3.6.1.2.1.1.2")
      assert SnmpOid.child_of?(oid1, oid2) == false
    end

    test "returns false for unrelated OIDs" do
      oid1 = SnmpOid.new!(".1.3.6.1.2.1.1.1")
      oid2 = SnmpOid.new!(".1.3.6.1.4.1.9999")
      assert SnmpOid.child_of?(oid1, oid2) == false
    end

    test "returns false for same OID" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1.1")
      assert SnmpOid.child_of?(oid, oid) == false
    end
  end

  describe "append/2" do
    test "appends single part to OID" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1")
      extended = SnmpOid.append(oid, [1])
      assert extended.oid == ".1.3.6.1.2.1.1.1"
      assert extended.parts == [1, 3, 6, 1, 2, 1, 1, 1]
    end

    test "appends multiple parts to OID" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1")
      extended = SnmpOid.append(oid, [1, 0])
      assert extended.oid == ".1.3.6.1.2.1.1.1.0"
      assert extended.parts == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end

    test "appending empty list returns same OID" do
      oid = SnmpOid.new!(".1.3.6.1.2.1.1.1")
      extended = SnmpOid.append(oid, [])
      assert extended.oid == ".1.3.6.1.2.1.1.1"
    end

    test "clears named field after appending" do
      oid = %SnmpOid{oid: ".1.3.6.1.2.1.1.1", named: "sysDescr", parts: [1, 3, 6, 1, 2, 1, 1, 1]}
      extended = SnmpOid.append(oid, [0])
      assert extended.named == nil
    end
  end

  describe "common SNMP OIDs" do
    test "system OIDs" do
      system_oid = SnmpOid.new!(".1.3.6.1.2.1.1")
      assert system_oid.parts == [1, 3, 6, 1, 2, 1, 1]
    end

    test "interfaces OIDs" do
      interfaces_oid = SnmpOid.new!(".1.3.6.1.2.1.2")
      assert interfaces_oid.parts == [1, 3, 6, 1, 2, 1, 2]
    end

    test "enterprise OIDs" do
      enterprises_oid = SnmpOid.new!(".1.3.6.1.4.1")
      assert enterprises_oid.parts == [1, 3, 6, 1, 4, 1]
    end

    test "can build instance OID by appending" do
      base = SnmpOid.new!(".1.3.6.1.2.1.1.1")
      instance = SnmpOid.append(base, [0])
      assert instance.oid == ".1.3.6.1.2.1.1.1.0"
    end
  end
end
