defmodule Towerops.Preseem.DeviceMatcherTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures
  import Towerops.DevicesFixtures
  import Towerops.OrganizationsFixtures

  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.DeviceMatcher
  alias Towerops.Preseem.Sync
  alias Towerops.Snmp.Device, as: SnmpDevice
  alias Towerops.Snmp.Interface

  setup do
    user = user_fixture()
    org = organization_fixture(user.id)

    %{org: org}
  end

  defp create_device_with_mac(org, ip_address, mac_address, opts \\ []) do
    name = Keyword.get(opts, :name, "Device #{System.unique_integer([:positive])}")

    device = device_fixture(%{organization_id: org.id, ip_address: ip_address, name: name})

    snmp_device =
      %SnmpDevice{}
      |> SnmpDevice.changeset(%{device_id: device.id, sys_name: name})
      |> Repo.insert!()

    _interface =
      %Interface{}
      |> Interface.changeset(%{
        snmp_device_id: snmp_device.id,
        if_index: 1,
        if_name: "eth0",
        if_phys_address: mac_address
      })
      |> Repo.insert!()

    device
  end

  defp create_preseem_ap(org_id, attrs) do
    ap_data =
      Map.merge(
        %{
          "element_uuid" => "preseem-#{System.unique_integer([:positive])}",
          "name" => "Test AP"
        },
        attrs
      )

    {:ok, ap} = Sync.upsert_access_point(org_id, ap_data)
    ap
  end

  describe "match_access_point/1" do
    test "matches by MAC address through interface -> snmp_device -> device", %{org: org} do
      _device = create_device_with_mac(org, "10.0.0.1", "aa:bb:cc:dd:ee:ff")

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-mac-match",
          "name" => "MAC Match AP",
          "system_mac_address" => "AA:BB:CC:DD:EE:FF"
        })

      assert {:ok, matched_ap} = DeviceMatcher.match_access_point(ap)
      assert matched_ap.match_confidence == "auto_mac"
      assert matched_ap.device_id
    end

    test "normalizes MAC addresses for matching (strips separators)", %{org: org} do
      _device = create_device_with_mac(org, "10.0.0.2", "11:22:33:44:55:66")

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-mac-normalize",
          "name" => "MAC Normalize AP",
          "system_mac_address" => "11-22-33-44-55-66"
        })

      assert {:ok, matched_ap} = DeviceMatcher.match_access_point(ap)
      assert matched_ap.match_confidence == "auto_mac"
    end

    test "matches by IP address when MAC does not match", %{org: org} do
      _device = device_fixture(%{organization_id: org.id, ip_address: "10.0.0.50", name: "IP Device"})

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-ip-match",
          "name" => "IP Match AP",
          "system_mac_address" => "FF:FF:FF:FF:FF:FF",
          "management_ip" => "10.0.0.50"
        })

      assert {:ok, matched_ap} = DeviceMatcher.match_access_point(ap)
      assert matched_ap.match_confidence == "auto_ip"
      assert matched_ap.device_id
    end

    test "matches by hostname when MAC and IP do not match", %{org: org} do
      _device =
        device_fixture(%{
          organization_id: org.id,
          ip_address: "192.168.99.99",
          name: "tower1-sector-a"
        })

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-hostname-match",
          "name" => "Tower1-Sector-A.local",
          "system_mac_address" => "FF:FF:FF:FF:FF:FE",
          "management_ip" => "172.16.0.99"
        })

      assert {:ok, matched_ap} = DeviceMatcher.match_access_point(ap)
      assert matched_ap.match_confidence == "auto_hostname"
      assert matched_ap.device_id
    end

    test "returns ambiguous when multiple devices match by MAC", %{org: org} do
      mac = "aa:aa:aa:aa:aa:aa"
      _device1 = create_device_with_mac(org, "10.0.1.1", mac, name: "Device A")
      _device2 = create_device_with_mac(org, "10.0.1.2", mac, name: "Device B")

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-ambiguous-mac",
          "name" => "Ambiguous MAC AP",
          "system_mac_address" => "AA:AA:AA:AA:AA:AA"
        })

      assert {:ok, matched_ap} = DeviceMatcher.match_access_point(ap)
      assert matched_ap.match_confidence == "ambiguous"
      assert matched_ap.device_id == nil
    end

    test "returns ambiguous when multiple devices match by IP", %{org: org} do
      _device1 =
        device_fixture(%{organization_id: org.id, ip_address: "10.0.2.1", name: "IP Device 1"})

      _device2 =
        device_fixture(%{organization_id: org.id, ip_address: "10.0.2.1", name: "IP Device 2"})

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-ambiguous-ip",
          "name" => "Ambiguous IP AP",
          "management_ip" => "10.0.2.1"
        })

      assert {:ok, matched_ap} = DeviceMatcher.match_access_point(ap)
      assert matched_ap.match_confidence == "ambiguous"
      assert matched_ap.device_id == nil
    end

    test "skips manually matched access points", %{org: org} do
      device = device_fixture(%{organization_id: org.id, ip_address: "10.0.3.1", name: "Manual Device"})

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-manual",
          "name" => "Manual AP",
          "management_ip" => "10.0.3.1"
        })

      # Manually set the match
      ap
      |> AccessPoint.changeset(%{device_id: device.id, match_confidence: "manual"})
      |> Repo.update!()

      ap = Repo.get!(AccessPoint, ap.id)

      assert {:ok, unchanged_ap} = DeviceMatcher.match_access_point(ap)
      assert unchanged_ap.match_confidence == "manual"
      assert unchanged_ap.device_id == device.id
    end

    test "returns unmatched when no devices match", %{org: org} do
      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-no-match",
          "name" => "Unmatched AP",
          "system_mac_address" => "00:00:00:00:00:01",
          "management_ip" => "172.31.255.255"
        })

      assert {:ok, unmatched_ap} = DeviceMatcher.match_access_point(ap)
      assert unmatched_ap.match_confidence == "unmatched"
      assert unmatched_ap.device_id == nil
    end
  end

  describe "match_unmatched/1" do
    test "processes all unmatched APs in an organization", %{org: org} do
      _device1 = device_fixture(%{organization_id: org.id, ip_address: "10.0.10.1", name: "Bulk Device 1"})
      _device2 = device_fixture(%{organization_id: org.id, ip_address: "10.0.10.2", name: "Bulk Device 2"})

      _ap1 =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-bulk-1",
          "name" => "Bulk AP 1",
          "management_ip" => "10.0.10.1"
        })

      _ap2 =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-bulk-2",
          "name" => "Bulk AP 2",
          "management_ip" => "10.0.10.2"
        })

      _ap3 =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-bulk-3",
          "name" => "Bulk AP 3 No Match",
          "management_ip" => "172.16.0.100"
        })

      assert {:ok, matched_count} = DeviceMatcher.match_unmatched(org.id)
      assert matched_count == 2

      # Verify the matched APs have device_ids
      aps = Repo.all(from ap in AccessPoint, where: ap.organization_id == ^org.id, order_by: :preseem_id)
      matched_aps = Enum.filter(aps, &(&1.device_id != nil))
      assert length(matched_aps) == 2
    end

    test "does not reprocess already matched APs", %{org: org} do
      device = device_fixture(%{organization_id: org.id, ip_address: "10.0.11.1", name: "Already Matched"})

      ap =
        create_preseem_ap(org.id, %{
          "element_uuid" => "ap-already-matched",
          "name" => "Already Matched AP",
          "management_ip" => "10.0.11.1"
        })

      # Manually match it first
      ap
      |> AccessPoint.changeset(%{device_id: device.id, match_confidence: "manual"})
      |> Repo.update!()

      assert {:ok, matched_count} = DeviceMatcher.match_unmatched(org.id)
      assert matched_count == 0
    end
  end
end
