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

  import Mox
  import Towerops.AccountsFixtures

  alias Towerops.Devices.Device
  alias Towerops.Snmp.Device
  alias Towerops.Snmp.Discovery
  alias Towerops.Snmp.Interface
  alias Towerops.Snmp.Sensor
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

  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
      })

    %{organization: organization, site: site}
  end

  describe "discover_device/1" do
    test "returns error when SNMP is not enabled", %{site: site} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: false
        })

      assert {:error, :snmp_not_enabled} = Discovery.discover_device(device)
    end

    test "successfully discovers MikroTik device", %{site: site} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "MikroTik Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      # Mock successful connection test
      expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
        {:ok, {:timeticks, 12_345}}
      end)

      # Mock system info discovery
      expect(SnmpMock, :get, 6, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "RouterOS RB750"}}

          "1.3.6.1.2.1.1.2.0" ->
            {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 14_988, 1]}}

          "1.3.6.1.2.1.1.3.0" ->
            {:ok, {:timeticks, 12_345}}

          "1.3.6.1.2.1.1.4.0" ->
            {:ok, {:octet_string, "admin@example.com"}}

          "1.3.6.1.2.1.1.5.0" ->
            {:ok, {:octet_string, "router1"}}

          "1.3.6.1.2.1.1.6.0" ->
            {:ok, {:octet_string, "Data Center"}}

          _ ->
            {:error, :no_such_object}
        end
      end)

      # Mock interface discovery
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.2.1.2.2.1.1.1", value: {:integer, 1}},
           %{oid: "1.3.6.1.2.1.2.2.1.1.2", value: {:integer, 2}}
         ]}
      end)

      # Mock interface data fetches for each interface
      expect(SnmpMock, :get, 16, fn _, oid, _ ->
        cond do
          String.ends_with?(oid, ".1") ->
            parts = String.split(oid, ".")
            second_to_last = Enum.at(parts, -2)

            cond do
              second_to_last == "2" -> {:ok, {:octet_string, "ether1"}}
              second_to_last == "3" -> {:ok, {:integer, 6}}
              second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
              second_to_last == "6" -> {:ok, {:octet_string, <<0x00, 0x11, 0x22, 0x33, 0x44, 0x55>>}}
              second_to_last == "7" -> {:ok, {:integer, 1}}
              second_to_last == "8" -> {:ok, {:integer, 1}}
              second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "ether1"}}
              second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "WAN"}}
              true -> {:error, :no_such_object}
            end

          String.ends_with?(oid, ".2") ->
            parts = String.split(oid, ".")
            second_to_last = Enum.at(parts, -2)

            cond do
              second_to_last == "2" -> {:ok, {:octet_string, "ether2"}}
              second_to_last == "3" -> {:ok, {:integer, 6}}
              second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
              second_to_last == "6" -> {:ok, {:octet_string, <<0x00, 0x11, 0x22, 0x33, 0x44, 0x56>>}}
              second_to_last == "7" -> {:ok, {:integer, 1}}
              second_to_last == "8" -> {:ok, {:integer, 1}}
              second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "ether2"}}
              second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "LAN"}}
              true -> {:error, :no_such_object}
            end

          true ->
            {:error, :no_such_object}
        end
      end)

      # Mock MikroTik health sensor discovery (15 sensors: voltages, temps, power, fans)
      # All MikroTik-specific OIDs will return no_such_object since device doesn't have them
      expect(SnmpMock, :get, 15, fn _, _, _ ->
        {:error, :no_such_object}
      end)

      # Mock sensor discovery - MikroTik profiles make many walk calls for sensors
      # CPU (1), storage (3), POE (4), optical (5), ENTITY-SENSOR-MIB (1) = 14 walks
      stub(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.99") do
          {:ok, []}
        else
          {:error, :no_such_object}
        end
      end)

      assert {:ok, device} = Discovery.discover_device(device)

      assert device.manufacturer == "MikroTik"
      assert device.model == "RB750"
      assert device.sys_name == "router1"

      # Verify interfaces were created
      interfaces = Repo.all(from i in Interface, where: i.snmp_device_id == ^device.id)
      assert length(interfaces) == 2

      interface1 = Enum.find(interfaces, &(&1.if_index == 1))
      assert interface1.if_descr == "ether1"
      assert interface1.if_name == "ether1"
      assert interface1.if_alias == "WAN"
    end

    test "successfully discovers Cisco device", %{site: site} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Cisco Switch",
          ip_address: "192.168.1.2",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      # Mock successful connection test
      expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
        {:ok, {:timeticks, 54_321}}
      end)

      # Mock system info discovery
      expect(SnmpMock, :get, 6, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Cisco IOS Software, C2960 Software"}}

          "1.3.6.1.2.1.1.2.0" ->
            {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 9, 1, 1208]}}

          "1.3.6.1.2.1.1.3.0" ->
            {:ok, {:timeticks, 54_321}}

          "1.3.6.1.2.1.1.4.0" ->
            {:ok, {:octet_string, "netadmin@cisco.com"}}

          "1.3.6.1.2.1.1.5.0" ->
            {:ok, {:octet_string, "switch1"}}

          "1.3.6.1.2.1.1.6.0" ->
            {:ok, {:octet_string, "Building A"}}

          _ ->
            {:error, :no_such_object}
        end
      end)

      # Mock interface discovery - no interfaces for simplicity
      # Walk called 5 times: interfaces (1), cisco sensors (1), base sensors fallback (1),
      # LLDP neighbors (1), CDP neighbors (1)
      expect(SnmpMock, :walk, 5, fn _, _, _ ->
        {:ok, []}
      end)

      assert {:ok, device} = Discovery.discover_device(device)

      assert device.manufacturer == "Cisco"
      assert device.model == "C2960"
      assert device.sys_name == "switch1"
    end

    test "handles connection failure", %{site: site} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Unreachable Device",
          ip_address: "192.168.1.99",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      # Mock failed connection test
      expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
        {:error, :timeout}
      end)

      assert {:error, :timeout} = Discovery.discover_device(device)
    end

    test "handles partial discovery failure gracefully", %{site: site} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Partial Device",
          ip_address: "192.168.1.3",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      # Mock successful connection test
      expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
        {:ok, {:timeticks, 999}}
      end)

      # Mock system info discovery
      expect(SnmpMock, :get, 6, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Generic Device"}}

          _ ->
            {:ok, {:octet_string, ""}}
        end
      end)

      # Mock failed interface discovery
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
        {:error, :timeout}
      end)

      # Mock failed sensor discovery and neighbor discovery (LLDP + CDP)
      expect(SnmpMock, :walk, 3, fn _, oid, _ ->
        cond do
          String.starts_with?(oid, "1.3.6.1.2.1.99") ->
            {:error, :timeout}

          String.starts_with?(oid, "1.0.8802") ->
            # LLDP - return empty
            {:ok, []}

          String.starts_with?(oid, "1.3.6.1.4.1.9.9.23") ->
            # CDP - return empty
            {:ok, []}

          true ->
            {:error, :no_such_object}
        end
      end)

      # Should succeed even with failed interface/sensor discovery
      assert {:ok, device} = Discovery.discover_device(device)

      assert device.manufacturer == "Unknown"

      # Verify no interfaces or sensors were created
      interfaces = Repo.all(from i in Interface, where: i.snmp_device_id == ^device.id)
      sensors = Repo.all(from s in Sensor, where: s.snmp_device_id == ^device.id)

      assert interfaces == []
      assert sensors == []
    end

    test "updates existing device on re-discovery", %{site: site} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Existing Device",
          ip_address: "192.168.1.4",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      # Create initial device
      {:ok, initial_device} =
        %Device{}
        |> Device.changeset(%{
          device_id: device.id,
          manufacturer: "Old Manufacturer",
          model: "Old Model",
          sys_name: "old-name"
        })
        |> Repo.insert()

      # Mock connection and system info for re-discovery
      expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
        {:ok, {:timeticks, 12_345}}
      end)

      expect(SnmpMock, :get, 6, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Linux server 5.4.0"}}

          "1.3.6.1.2.1.1.5.0" ->
            {:ok, {:octet_string, "updated-name"}}

          _ ->
            {:ok, {:octet_string, ""}}
        end
      end)

      # NetSnmp also tries UCD sensors (load, memory, disk) which make additional get calls
      stub(SnmpMock, :get, fn _, _, _ ->
        {:error, :no_such_object}
      end)

      # NetSnmp profile makes many walk calls for sensors
      # Interfaces (1), LM sensors (6), UCD sensors (3), ENTITY-SENSOR-MIB fallback (1)
      stub(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      assert {:ok, updated_device} = Discovery.discover_device(device)

      # Should be same device, updated
      assert updated_device.id == initial_device.id
      assert updated_device.manufacturer == "Linux"
      assert updated_device.model == "Linux"
      assert updated_device.sys_name == "updated-name"
    end
  end

  describe "discover_all/1" do
    test "discovers multiple devices concurrently", %{organization: organization, site: site} do
      # Create multiple SNMP-enabled devices
      {:ok, device1} =
        Towerops.Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.10",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      {:ok, device2} =
        Towerops.Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.11",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      {:ok, _device3} =
        Towerops.Devices.create_device(%{
          name: "Device 3 - No SNMP",
          ip_address: "192.168.1.12",
          site_id: site.id,
          snmp_enabled: false
        })

      # Mock successful discovery for device1
      allow(SnmpMock, self(), fn -> :ok end)

      stub(SnmpMock, :get, fn _target, oid, _opts ->
        case oid do
          "1.3.6.1.2.1.1.3.0" ->
            {:ok, {:timeticks, 12_345}}

          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Test Device"}}

          _ ->
            {:ok, {:octet_string, ""}}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      assert {:ok, summary} = Discovery.discover_all(organization.id)

      # Should only discover SNMP-enabled devices
      assert summary.success == 2
      assert summary.failed == 0

      # Verify devices were created
      assert Repo.get_by(Device, device_id: device1.id)
      assert Repo.get_by(Device, device_id: device2.id)
    end

    test "handles mixed success and failure", %{organization: organization, site: site} do
      {:ok, device1} =
        Towerops.Devices.create_device(%{
          name: "Good Device",
          ip_address: "192.168.1.20",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      {:ok, _device2} =
        Towerops.Devices.create_device(%{
          name: "Bad Device",
          ip_address: "192.168.1.21",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "wrong",
          snmp_port: 161
        })

      allow(SnmpMock, self(), fn -> :ok end)

      stub(SnmpMock, :get, fn target, oid, _opts ->
        # Fail for .21, succeed for .20
        if target == "192.168.1.21" do
          {:error, :auth_failure}
        else
          case oid do
            "1.3.6.1.2.1.1.3.0" -> {:ok, {:timeticks, 12_345}}
            "1.3.6.1.2.1.1.1.0" -> {:ok, {:octet_string, "Good Device"}}
            _ -> {:ok, {:octet_string, ""}}
          end
        end
      end)

      stub(SnmpMock, :walk, fn target, _, _ ->
        if target == "192.168.1.21" do
          {:error, :auth_failure}
        else
          {:ok, []}
        end
      end)

      assert {:ok, summary} = Discovery.discover_all(organization.id)

      assert summary.success == 1
      assert summary.failed == 1
      assert :auth_failure in summary.errors

      # Verify only good device was created
      assert Repo.get_by(Device, device_id: device1.id)
    end

    test "updates device name from SNMP sysName when device name is empty", %{
      organization: _organization,
      site: site
    } do
      # Create device without a name
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: nil,
          ip_address: "192.168.1.200",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      # Mock successful connection test
      expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
        {:ok, {:timeticks, 12_345}}
      end)

      # Mock system info discovery with sysName + UCD sensor OIDs
      # 6 system info OIDs + 3 load OIDs + 2 memory OIDs = 11 total
      expect(SnmpMock, :get, 11, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Linux switch01 4.4.0"}}

          "1.3.6.1.2.1.1.2.0" ->
            {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 8072, 3, 2, 10]}}

          "1.3.6.1.2.1.1.3.0" ->
            {:ok, {:timeticks, 12_345}}

          "1.3.6.1.2.1.1.4.0" ->
            {:ok, {:octet_string, "admin@example.com"}}

          "1.3.6.1.2.1.1.5.0" ->
            {:ok, {:octet_string, "discovered-switch-name"}}

          "1.3.6.1.2.1.1.6.0" ->
            {:ok, {:octet_string, "Server Room"}}

          _ ->
            {:error, :no_such_object}
        end
      end)

      # Mock interface and sensor discovery - no interfaces/sensors for simplicity
      # Walk called 10 times:
      # - LM sensors: temp devices (1), temp values (1), fan devices (1), fan values (1),
      #   voltage devices (1), voltage values (1)
      # - Base sensors fallback (1)
      # - Interfaces (1)
      # - Neighbors: LLDP (1), CDP (1)
      expect(SnmpMock, :walk, 10, fn _, _, _ ->
        {:ok, []}
      end)

      assert {:ok, _snmp_device} = Discovery.discover_device(device)

      # Verify the device name was updated with sysName
      updated_device = Towerops.Devices.get_device!(device.id)
      assert updated_device.name == "discovered-switch-name"
    end

    test "does not update device name from SNMP when device already has a name", %{
      organization: _organization,
      site: site
    } do
      # Create device with a name
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "My Custom Name",
          ip_address: "192.168.1.201",
          site_id: site.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      # Mock successful connection test
      expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
        {:ok, {:timeticks, 12_345}}
      end)

      # Mock system info discovery with different sysName + UCD sensor OIDs
      # 6 system info OIDs + 3 load OIDs + 2 memory OIDs = 11 total
      expect(SnmpMock, :get, 11, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Linux switch02 4.4.0"}}

          "1.3.6.1.2.1.1.2.0" ->
            {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 8072, 3, 2, 10]}}

          "1.3.6.1.2.1.1.3.0" ->
            {:ok, {:timeticks, 12_345}}

          "1.3.6.1.2.1.1.4.0" ->
            {:ok, {:octet_string, "admin@example.com"}}

          "1.3.6.1.2.1.1.5.0" ->
            {:ok, {:octet_string, "different-switch-name"}}

          "1.3.6.1.2.1.1.6.0" ->
            {:ok, {:octet_string, "Server Room"}}

          _ ->
            {:error, :no_such_object}
        end
      end)

      # Mock interface and sensor discovery
      # Walk called 10 times:
      # - LM sensors: temp devices (1), temp values (1), fan devices (1), fan values (1),
      #   voltage devices (1), voltage values (1)
      # - Base sensors fallback (1)
      # - Interfaces (1)
      # - Neighbors: LLDP (1), CDP (1)
      expect(SnmpMock, :walk, 10, fn _, _, _ ->
        {:ok, []}
      end)

      assert {:ok, _snmp_device} = Discovery.discover_device(device)

      # Verify the device name was NOT changed
      updated_device = Towerops.Devices.get_device!(device.id)
      assert updated_device.name == "My Custom Name"
    end
  end
end
