defmodule Towerops.Snmp.NeighborCleanupWorkerTest do
  use Towerops.DataCase, async: false

  import Towerops.AccountsFixtures

  alias Towerops.Snmp
  alias Towerops.Snmp.ArpEntry
  alias Towerops.Snmp.Device
  alias Towerops.Snmp.Interface
  alias Towerops.Snmp.Neighbor
  alias Towerops.Snmp.NeighborCleanupWorker

  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_schema1} =
      Towerops.Devices.create_device(%{
        name: "Test Switch 1",
        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
      })

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

    device1 =
      %Device{}
      |> Device.changeset(%{
        device_id: device_schema1.id,
        sys_name: "test-switch-1",
        sys_descr: "Test Switch 1"
      })
      |> Repo.insert!()

    device2 =
      %Device{}
      |> Device.changeset(%{
        device_id: device_schema2.id,
        sys_name: "test-switch-2",
        sys_descr: "Test Switch 2"
      })
      |> Repo.insert!()

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

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

    %{
      device_schema1: device_schema1,
      device_schema2: device_schema2,
      device1: device1,
      device2: device2,
      interface1: interface1,
      interface2: interface2
    }
  end

  describe "cleanup process" do
    test "cleanup removes stale neighbors across all devices", %{
      device_schema1: device_schema1,
      device_schema2: device_schema2,
      device1: _device1,
      device2: _device2,
      interface1: interface1,
      interface2: interface2
    } do
      # Create old neighbors on both devices
      two_days_ago = DateTime.add(DateTime.utc_now(), -48, :hour)
      one_hour_ago = DateTime.add(DateTime.utc_now(), -1, :hour)

      # Old neighbor on device1
      {:ok, old_neighbor1} =
        Snmp.upsert_neighbor(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          protocol: "lldp",
          remote_chassis_id: "old-neighbor-1",
          remote_system_name: "old-neighbor-1",
          last_discovered_at: two_days_ago
        })

      # Old neighbor on device2
      {:ok, old_neighbor2} =
        Snmp.upsert_neighbor(%{
          device_id: device_schema2.id,
          interface_id: interface2.id,
          protocol: "cdp",
          remote_chassis_id: "old-neighbor-2",
          remote_system_name: "old-neighbor-2",
          last_discovered_at: two_days_ago
        })

      # Recent neighbor on device1
      {:ok, recent_neighbor} =
        Snmp.upsert_neighbor(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          protocol: "cdp",
          remote_chassis_id: "recent-neighbor",
          remote_system_name: "recent-neighbor",
          last_discovered_at: one_hour_ago
        })

      # Trigger cleanup via Oban worker
      assert :ok = NeighborCleanupWorker.perform(%Oban.Job{args: %{}})

      # Old neighbors should be deleted
      assert Repo.get(Neighbor, old_neighbor1.id) == nil
      assert Repo.get(Neighbor, old_neighbor2.id) == nil

      # Recent neighbor should still exist
      assert Repo.get(Neighbor, recent_neighbor.id)
    end

    test "cleanup handles device with no neighbors gracefully", %{device1: device1} do
      # Ensure no neighbors exist
      assert Snmp.list_neighbors(device1.id) == []

      # Should not crash
      assert :ok = NeighborCleanupWorker.perform(%Oban.Job{args: %{}})
    end

    test "cleanup preserves neighbors within 24 hour threshold", %{
      device_schema1: device_schema1,
      device1: _device1,
      interface1: interface1
    } do
      # Create neighbors at various ages
      now = DateTime.utc_now()
      twenty_three_hours_ago = DateTime.add(now, -23, :hour)
      twenty_five_hours_ago = DateTime.add(now, -25, :hour)

      # Neighbor just within threshold
      {:ok, recent_neighbor} =
        Snmp.upsert_neighbor(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          protocol: "lldp",
          remote_chassis_id: "recent",
          remote_system_name: "recent",
          last_discovered_at: twenty_three_hours_ago
        })

      # Neighbor just outside threshold
      {:ok, stale_neighbor} =
        Snmp.upsert_neighbor(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          protocol: "cdp",
          remote_chassis_id: "stale",
          remote_system_name: "stale",
          last_discovered_at: twenty_five_hours_ago
        })

      assert :ok = NeighborCleanupWorker.perform(%Oban.Job{args: %{}})

      # Recent neighbor should still exist
      assert Repo.get(Neighbor, recent_neighbor.id)

      # Stale neighbor should be deleted
      assert Repo.get(Neighbor, stale_neighbor.id) == nil
    end

    test "cleanup does not affect device without SNMP enabled" do
      user = user_fixture()
      {:ok, org} = Towerops.Organizations.create_organization(%{name: "Test Org 2"}, user.id)

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

      # Create device without SNMP enabled
      {:ok, device_schema} =
        Towerops.Devices.create_device(%{
          name: "No SNMP Device",
          ip_address: "192.168.1.100",
          snmp_enabled: false,
          site_id: site.id,
          organization_id: org.id
        })

      device =
        %Device{}
        |> Device.changeset(%{
          device_id: device_schema.id,
          sys_name: "no-snmp-device",
          sys_descr: "No SNMP Device"
        })
        |> Repo.insert!()

      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: device.id,
          if_index: 1,
          if_descr: "eth0",
          if_type: 6
        })
        |> Repo.insert!()

      # Create old neighbor
      two_days_ago = DateTime.add(DateTime.utc_now(), -48, :hour)

      {:ok, neighbor} =
        Snmp.upsert_neighbor(%{
          device_id: device_schema.id,
          interface_id: interface.id,
          protocol: "lldp",
          remote_chassis_id: "neighbor",
          remote_system_name: "neighbor",
          last_discovered_at: two_days_ago
        })

      assert :ok = NeighborCleanupWorker.perform(%Oban.Job{args: %{}})

      # Neighbor should still exist (device not included in cleanup)
      # because cleanup only processes SNMP-enabled devices
      assert Repo.get(Neighbor, neighbor.id)
    end
  end

  describe "ARP cleanup" do
    test "cleanup removes stale ARP entries across all devices", %{
      device_schema1: device_schema1,
      device_schema2: device_schema2,
      interface1: interface1,
      interface2: interface2
    } do
      two_days_ago = DateTime.add(DateTime.utc_now(), -48, :hour)
      one_hour_ago = DateTime.add(DateTime.utc_now(), -1, :hour)

      # Old ARP entry on device1
      {:ok, old_arp1} =
        Snmp.upsert_arp_entry(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:01",
          entry_type: "dynamic",
          if_index: 1,
          last_seen_at: two_days_ago
        })

      # Old ARP entry on device2
      {:ok, old_arp2} =
        Snmp.upsert_arp_entry(%{
          device_id: device_schema2.id,
          interface_id: interface2.id,
          ip_address: "192.168.1.200",
          mac_address: "aa:bb:cc:dd:ee:02",
          entry_type: "dynamic",
          if_index: 1,
          last_seen_at: two_days_ago
        })

      # Recent ARP entry on device1
      {:ok, recent_arp} =
        Snmp.upsert_arp_entry(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          ip_address: "192.168.1.50",
          mac_address: "aa:bb:cc:dd:ee:03",
          entry_type: "static",
          if_index: 1,
          last_seen_at: one_hour_ago
        })

      assert :ok = NeighborCleanupWorker.perform(%Oban.Job{args: %{}})

      # Old ARP entries should be deleted
      assert Repo.get(ArpEntry, old_arp1.id) == nil
      assert Repo.get(ArpEntry, old_arp2.id) == nil

      # Recent ARP entry should still exist
      assert Repo.get(ArpEntry, recent_arp.id)
    end

    test "cleanup preserves ARP entries within 24 hour threshold", %{
      device_schema1: device_schema1,
      interface1: interface1
    } do
      now = DateTime.utc_now()
      twenty_three_hours_ago = DateTime.add(now, -23, :hour)
      twenty_five_hours_ago = DateTime.add(now, -25, :hour)

      # ARP entry just within threshold
      {:ok, recent_arp} =
        Snmp.upsert_arp_entry(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:01",
          entry_type: "dynamic",
          if_index: 1,
          last_seen_at: twenty_three_hours_ago
        })

      # ARP entry just outside threshold
      {:ok, stale_arp} =
        Snmp.upsert_arp_entry(%{
          device_id: device_schema1.id,
          interface_id: interface1.id,
          ip_address: "192.168.1.200",
          mac_address: "aa:bb:cc:dd:ee:02",
          entry_type: "dynamic",
          if_index: 1,
          last_seen_at: twenty_five_hours_ago
        })

      assert :ok = NeighborCleanupWorker.perform(%Oban.Job{args: %{}})

      # Recent ARP entry should still exist
      assert Repo.get(ArpEntry, recent_arp.id)

      # Stale ARP entry should be deleted
      assert Repo.get(ArpEntry, stale_arp.id) == nil
    end
  end
end
