defmodule Towerops.Snmp.NeighborTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures

  alias Towerops.Snmp.Device
  alias Towerops.Snmp.Interface
  alias Towerops.Snmp.Neighbor

  setup do
    user = user_fixture()

    {:ok, organization} =
      Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

    {:ok, site} =
      Towerops.Sites.create_site(%{
        name: "Test Site",
        organization_id: organization.id
      })

    {:ok, device_schema} =
      Towerops.Devices.create_device(%{
        name: "Test Router",
        ip_address: "192.168.1.1",
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public",
        snmp_port: 161,
        site_id: site.id,
        organization_id: organization.id
      })

    snmp_device =
      %Device{}
      |> Device.changeset(%{
        device_id: device_schema.id,
        sys_name: "test-router",
        sys_descr: "Test Router"
      })
      |> Repo.insert!()

    interface =
      %Interface{}
      |> Interface.changeset(%{
        snmp_device_id: snmp_device.id,
        if_index: 1,
        if_descr: "GigabitEthernet0/0",
        if_name: "Gi0/0",
        if_type: 6,
        if_oper_status: "up"
      })
      |> Repo.insert!()

    %{device: device_schema, interface: interface}
  end

  describe "changeset/2" do
    test "valid changeset with all required fields", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
    end

    test "valid changeset with all fields", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "cdp",
        remote_chassis_id: "00:1a:2b:3c:4d:5e",
        remote_system_name: "neighbor-switch",
        remote_system_description: "Cisco IOS Software",
        remote_platform: "Cisco 2960",
        remote_port_id: "GigabitEthernet1/0/1",
        remote_port_description: "Downlink to router",
        remote_address: "192.168.1.2",
        remote_capabilities: ["bridge", "router"],
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
      assert get_field(changeset, :remote_chassis_id) == "00:1a:2b:3c:4d:5e"
      assert get_field(changeset, :remote_capabilities) == ["bridge", "router"]
    end

    test "invalid changeset without device_id", %{interface: interface} do
      attrs = %{
        interface_id: interface.id,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).device_id
    end

    test "invalid changeset without interface_id", %{device: device} do
      attrs = %{
        device_id: device.id,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).interface_id
    end

    test "invalid changeset without protocol", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).protocol
    end

    test "invalid changeset without last_discovered_at", %{
      device: device,
      interface: interface
    } do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp"
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      refute changeset.valid?
      assert "can't be blank" in errors_on(changeset).last_discovered_at
    end

    test "invalid changeset with invalid protocol", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "invalid",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      refute changeset.valid?
      assert "is invalid" in errors_on(changeset).protocol
    end

    test "valid changeset with 'lldp' protocol", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
    end

    test "valid changeset with 'cdp' protocol", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "cdp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
    end

    test "valid changeset with nil optional fields", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        remote_chassis_id: nil,
        remote_system_name: nil,
        remote_system_description: nil,
        remote_platform: nil,
        remote_port_id: nil,
        remote_port_description: nil,
        remote_address: nil,
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
    end

    test "default remote_capabilities is empty list", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
      assert get_field(changeset, :remote_capabilities) == []
    end

    test "normalizes binary UUID device_id to string", %{device: device, interface: interface} do
      # Create a 16-byte binary UUID
      {:ok, binary_uuid} = Ecto.UUID.dump(device.id)

      attrs = %{
        device_id: binary_uuid,
        interface_id: interface.id,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
      # The normalized UUID should be a string format
      assert get_change(changeset, :device_id) == device.id
    end

    test "normalizes binary UUID interface_id to string", %{device: device, interface: interface} do
      # Create a 16-byte binary UUID
      {:ok, binary_uuid} = Ecto.UUID.dump(interface.id)

      attrs = %{
        device_id: device.id,
        interface_id: binary_uuid,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
      # The normalized UUID should be a string format
      assert get_change(changeset, :interface_id) == interface.id
    end

    test "keeps string UUID unchanged", %{device: device, interface: interface} do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      }

      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert changeset.valid?
      assert get_change(changeset, :device_id) == device.id
      assert get_change(changeset, :interface_id) == interface.id
    end
  end

  describe "unique constraint" do
    test "prevents duplicate neighbors with same interface, chassis_id, and protocol", %{
      device: device,
      interface: interface
    } do
      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        remote_chassis_id: "00:1a:2b:3c:4d:5e",
        last_discovered_at: DateTime.utc_now()
      }

      # Insert first neighbor
      changeset = Neighbor.changeset(%Neighbor{}, attrs)
      assert {:ok, _neighbor} = Repo.insert(changeset)

      # Attempt to insert duplicate
      changeset2 = Neighbor.changeset(%Neighbor{}, attrs)

      assert {:error, changeset} = Repo.insert(changeset2)
      assert "has already been taken" in errors_on(changeset).interface_id
    end

    test "allows same chassis_id with different interface", %{
      device: device,
      interface: interface
    } do
      # Read the SNMP device to get its ID
      snmp_device = Repo.get_by!(Device, device_id: device.id)

      # Create a second interface
      interface2 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_descr: "GigabitEthernet0/1",
          if_name: "Gi0/1",
          if_type: 6,
          if_oper_status: "up"
        })
        |> Repo.insert!()

      attrs1 = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        remote_chassis_id: "00:1a:2b:3c:4d:5e",
        last_discovered_at: DateTime.utc_now()
      }

      attrs2 = %{
        device_id: device.id,
        interface_id: interface2.id,
        protocol: "lldp",
        remote_chassis_id: "00:1a:2b:3c:4d:5e",
        last_discovered_at: DateTime.utc_now()
      }

      # Insert neighbors on different interfaces
      changeset1 = Neighbor.changeset(%Neighbor{}, attrs1)
      assert {:ok, _neighbor1} = Repo.insert(changeset1)

      changeset2 = Neighbor.changeset(%Neighbor{}, attrs2)
      assert {:ok, _neighbor2} = Repo.insert(changeset2)
    end

    test "allows same chassis_id and interface with different protocol", %{
      device: device,
      interface: interface
    } do
      attrs1 = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        remote_chassis_id: "00:1a:2b:3c:4d:5e",
        last_discovered_at: DateTime.utc_now()
      }

      attrs2 = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "cdp",
        remote_chassis_id: "00:1a:2b:3c:4d:5e",
        last_discovered_at: DateTime.utc_now()
      }

      # Insert neighbors with different protocols
      changeset1 = Neighbor.changeset(%Neighbor{}, attrs1)
      assert {:ok, _neighbor1} = Repo.insert(changeset1)

      changeset2 = Neighbor.changeset(%Neighbor{}, attrs2)
      assert {:ok, _neighbor2} = Repo.insert(changeset2)
    end
  end
end
