defmodule Towerops.Workers.DiscoveryWorkerTest do
  use Towerops.DataCase, async: true

  import Mox
  import Towerops.AccountsFixtures

  alias Towerops.Snmp
  alias Towerops.Snmp.SnmpMock
  alias Towerops.Workers.DiscoveryWorker

  setup :verify_on_exit!

  describe "perform/1" do
    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} =
        Towerops.Devices.create_device(%{
          name: "Test Device",
          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: device, org: organization}
    end

    test "successfully discovers a device", %{device: device} do
      # Mock SNMP responses - use stub for flexibility as discovery calls vary
      stub(SnmpMock, :get, fn _target, oid, _opts ->
        case oid do
          "1.3.6.1.2.1.1.1.0" -> {:ok, "Test Device"}
          "1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1, 4, 1, 9]}
          "1.3.6.1.2.1.1.3.0" -> {:ok, 12_345}
          "1.3.6.1.2.1.1.4.0" -> {:ok, "admin@test.com"}
          "1.3.6.1.2.1.1.5.0" -> {:ok, "test-device"}
          "1.3.6.1.2.1.1.6.0" -> {:ok, "Test Location"}
          # UCD-SNMP-MIB CPU stats (processor discovery fallback)
          "1.3.6.1.4.1.2021.11.9.0" -> {:ok, 10}
          "1.3.6.1.4.1.2021.11.10.0" -> {:ok, 5}
          "1.3.6.1.4.1.2021.11.11.0" -> {:ok, 85}
          _ -> {:error, :timeout}
        end
      end)

      # Mock interface discovery (if_index walk + other IF-MIB walks) and neighbor discovery
      # Total: if_index + if_descr + if_type + if_speed + if_phys_address + if_admin_status +
      #        if_oper_status + if_name + if_alias (9 walks) + LLDP (6 walks) + CDP (1 walk) = 16 walks
      # Simplify: just allow any number of walk calls to return empty
      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:ok, []}
      end)

      assert :ok = DiscoveryWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})

      # Verify SNMP device was created
      snmp_device = Snmp.get_device(device.id)
      assert snmp_device
      assert snmp_device.sys_descr == "Test Device"
      assert snmp_device.sys_name == "test-device"
    end

    test "returns :discard when device not found" do
      non_existent_id = Ecto.UUID.generate()

      assert :discard =
               DiscoveryWorker.perform(%Oban.Job{args: %{"device_id" => non_existent_id}})
    end

    test "returns error for transient failures to allow retry", %{device: device} do
      # Mock SNMP failure with a transient error (timeout)
      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :timeout}
      end)

      # Transient failures should return {:error, reason} so Oban can retry
      assert {:error, _reason} = DiscoveryWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
    end

    test "successfully completes discovery with proper mocks", %{device: device} do
      # Mock SNMP responses - use stub for flexibility as discovery calls vary
      stub(SnmpMock, :get, fn _target, oid, _opts ->
        case oid do
          "1.3.6.1.2.1.1.1.0" -> {:ok, "Test"}
          "1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1]}
          "1.3.6.1.2.1.1.3.0" -> {:ok, 1}
          "1.3.6.1.2.1.1.4.0" -> {:ok, ""}
          "1.3.6.1.2.1.1.5.0" -> {:ok, "test"}
          "1.3.6.1.2.1.1.6.0" -> {:ok, ""}
          # UCD-SNMP-MIB CPU stats (processor discovery fallback)
          "1.3.6.1.4.1.2021.11.9.0" -> {:ok, 10}
          "1.3.6.1.4.1.2021.11.10.0" -> {:ok, 5}
          "1.3.6.1.4.1.2021.11.11.0" -> {:ok, 85}
          _ -> {:error, :timeout}
        end
      end)

      # Allow any number of walk calls (interface + neighbor discovery)
      stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:ok, []} end)

      assert :ok = DiscoveryWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})

      # Verify SNMP device was created
      snmp_device = Snmp.get_device(device.id)
      assert snmp_device
    end

    test "logs error when discovery fails", %{device: device} do
      import ExUnit.CaptureLog

      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :network_unreachable}
      end)

      log =
        capture_log(fn ->
          DiscoveryWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
        end)

      assert log =~ "Direct SNMP discovery failed:"
    end
  end

  describe "enqueue/1" do
    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Enqueue Org"}, user.id)

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Enqueue Device",
          ip_address: "192.168.1.50",
          snmp_enabled: true,
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device: device}
    end

    test "enqueues a discovery job", %{device: device} do
      assert :ok = DiscoveryWorker.enqueue(device.id)
    end

    test "deduplicates jobs within 60 seconds", %{device: device} do
      assert :ok = DiscoveryWorker.enqueue(device.id)
      assert :ok = DiscoveryWorker.enqueue(device.id)
    end
  end

  describe "retryable_error?/1" do
    test "timeout is retryable" do
      assert DiscoveryWorker.retryable_error?(:timeout)
    end

    test "network_unreachable is retryable" do
      assert DiscoveryWorker.retryable_error?(:network_unreachable)
    end

    test "connection_refused is retryable" do
      assert DiscoveryWorker.retryable_error?(:connection_refused)
    end

    test "econnrefused is retryable" do
      assert DiscoveryWorker.retryable_error?(:econnrefused)
    end

    test "device_not_found is not retryable" do
      refute DiscoveryWorker.retryable_error?(:device_not_found)
    end

    test "invalid_config is not retryable" do
      refute DiscoveryWorker.retryable_error?(:invalid_config)
    end

    test "no_snmp_credentials is not retryable" do
      refute DiscoveryWorker.retryable_error?(:no_snmp_credentials)
    end

    test "device_deleted is not retryable" do
      refute DiscoveryWorker.retryable_error?(:device_deleted)
    end

    test "unknown errors are retryable by default" do
      assert DiscoveryWorker.retryable_error?(:some_unknown_error)
    end
  end
end
