defmodule Towerops.Workers.DevicePollerWorkerTest do
  use Towerops.DataCase, async: false

  # DevicePollerWorker is deprecated in favor of CheckExecutorWorker
  # These tests remain for documentation but are skipped
  import Mox
  import Towerops.AccountsFixtures

  alias Towerops.Devices
  alias Towerops.Organizations
  alias Towerops.Repo
  alias Towerops.Sites
  alias Towerops.Snmp.Device
  alias Towerops.Snmp.Interface
  alias Towerops.Snmp.Sensor
  alias Towerops.Snmp.SnmpMock
  alias Towerops.Snmp.StateSensor
  alias Towerops.Workers.DevicePollerWorker
  alias Towerops.Workers.PollingOffset

  @moduletag :skip

  setup :verify_on_exit!

  setup do
    # Configure Mock SNMP Adapter
    old_adapter = Application.get_env(:towerops, :snmp_adapter)
    Application.put_env(:towerops, :snmp_adapter, SnmpMock)

    # Disable Phoenix SNMP check for tests
    old_disable = Application.get_env(:towerops, :disable_phoenix_snmp)
    Application.put_env(:towerops, :disable_phoenix_snmp, false)

    on_exit(fn ->
      if old_adapter do
        Application.put_env(:towerops, :snmp_adapter, old_adapter)
      else
        Application.delete_env(:towerops, :snmp_adapter)
      end

      if old_disable == nil do
        Application.delete_env(:towerops, :disable_phoenix_snmp)
      else
        Application.put_env(:towerops, :disable_phoenix_snmp, old_disable)
      end
    end)

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

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

    %{organization: organization, site: site, user: user}
  end

  describe "perform/1" do
    test "returns :ok when device does not exist" do
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => Ecto.UUID.generate()}})
    end

    test "skips polling when snmp_enabled is false", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router No SNMP",
          ip_address: "192.168.1.10",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: false
        })

      # Ensure no jobs exist initially (created by create_device)
      Repo.delete_all(Oban.Job)

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

      # FIXED: Should NOT schedule next poll when SNMP is disabled
      # This prevents zombie jobs from continuing to poll disabled devices
      # Previously, it would reschedule even when disabled, causing unnecessary work
      assert Repo.aggregate(Oban.Job, :count) == 0
    end

    test "polls device and schedules next run", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router Poll",
          ip_address: "192.168.1.11",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          check_interval_seconds: 60
        })

      # Clear existing jobs
      Repo.delete_all(Oban.Job)

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

      # Verify next job scheduled
      assert Repo.aggregate(Oban.Job, :count) == 1
    end
  end

  describe "polling logic" do
    test "polls sensors and updates values", %{site: site} do
      # 1. Create Device
      {:ok, device} =
        Devices.create_device(%{
          name: "Sensor Device",
          ip_address: "192.168.1.50",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true
        })

      # 2. Create SnmpDevice association
      {:ok, snmp_device} =
        Repo.insert(%Device{
          device_id: device.id,
          sys_object_id: "1.3.6.1.4.1.9.1",
          sys_descr: "Cisco Router",
          sys_name: "router01"
        })

      # 3. Create Sensor
      {:ok, sensor} =
        Repo.insert(%Sensor{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1",
          sensor_descr: "Chassis Temp",
          sensor_unit: "C",
          sensor_divisor: 1,
          monitored: true
        })

      # 4. Create Interface
      {:ok, interface} =
        Repo.insert(%Interface{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_name: "eth0",
          if_descr: "Ethernet 0",
          if_type: 6,
          if_speed: 100_000_000,
          if_phys_address: "00:11:22:33:44:55",
          if_admin_status: "up",
          if_oper_status: "up",
          monitored: true
        })

      # 4b. Create State Sensor
      {:ok, state_sensor} =
        Repo.insert(%StateSensor{
          snmp_device_id: snmp_device.id,
          sensor_descr: "Power Supply 1",
          sensor_oid: "1.3.6.1.4.1.9.9.13.1.5.1.3.1",
          sensor_index: "1",
          metadata: %{"states" => %{"1" => "unknown", "2" => "enabled", "3" => "disabled"}}
          # status defaults to unknown
        })

      # 4c. Create Processor
      {:ok, processor} =
        Repo.insert(%Towerops.Snmp.Processor{
          snmp_device_id: snmp_device.id,
          processor_index: "1",
          processor_type: "hr_processor",
          description: "CPU 1"
        })

      # 5. Mock SNMP response
      # The worker runs tasks in parallel, so we need to be careful with expectations.
      # It calls poll_device_sensors, poll_device_state_sensors, etc.
      # We only have one sensor, so poll_device_sensors will try to fetch it.

      SnmpMock
      |> stub(:get, fn _target, oid, _opts ->
        case oid do
          # Sensor
          "1.3.6.1.2.1.99.1.1.1.4.1" -> {:ok, 45}
          # State Sensor
          # enabled/ok
          "1.3.6.1.4.1.9.9.13.1.5.1.3.1" -> {:ok, 2}
          # Processor Load
          # 25% load
          "1.3.6.1.2.1.25.3.3.1.2.1" -> {:ok, 25}
          # Interface HC In Octets
          "1.3.6.1.2.1.31.1.1.1.6.2" -> {:ok, 1000}
          # Interface HC Out Octets
          "1.3.6.1.2.1.31.1.1.1.10.2" -> {:ok, 2000}
          # Interface Errors
          "1.3.6.1.2.1.2.2.1.14.2" -> {:ok, 0}
          "1.3.6.1.2.1.2.2.1.20.2" -> {:ok, 0}
          "1.3.6.1.2.1.2.2.1.13.2" -> {:ok, 0}
          "1.3.6.1.2.1.2.2.1.19.2" -> {:ok, 0}
          # Interface change detection OIDs (for interface 2)
          # Speed
          "1.3.6.1.2.1.2.2.1.5.2" -> {:ok, 100_000_000}
          # Physical address (MAC)
          "1.3.6.1.2.1.2.2.1.6.2" -> {:ok, <<0, 17, 34, 51, 68, 85>>}
          # Admin status (down=2)
          "1.3.6.1.2.1.2.2.1.7.2" -> {:ok, 2}
          # Oper status (up=1)
          "1.3.6.1.2.1.2.2.1.8.2" -> {:ok, 1}
          _ -> {:error, :no_such_object}
        end
      end)
      # Allow other calls (like interfaces, neighbors etc) to return empty/ok
      |> stub(:walk, fn _target, _oid, _opts -> {:ok, []} end)

      # 6. Run Perform
      Repo.delete_all(Oban.Job)
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})

      # 7. Verify Sensor Updated
      updated_sensor = Repo.get(Sensor, sensor.id)
      assert updated_sensor.last_value == 45.0
      assert updated_sensor.last_checked_at

      # 8. Verify Interface Stats
      stats = Repo.all(Towerops.Snmp.InterfaceStat)
      refute Enum.empty?(stats)
      stat = List.last(stats)
      assert stat.interface_id == interface.id
      assert stat.if_in_octets == 1000
      assert stat.if_out_octets == 2000

      # 9. Verify Interface Changed
      updated_interface = Repo.get(Interface, interface.id)
      assert updated_interface.if_admin_status == "down"

      # 10. Verify State Sensor
      updated_state = Repo.get(StateSensor, state_sensor.id)
      assert updated_state.state_value == 2
      assert updated_state.status == "ok"
      assert updated_state.state_descr == "enabled"

      # 11. Verify Processor
      readings = Repo.all(Towerops.Snmp.ProcessorReading)
      refute Enum.empty?(readings)
      reading = List.last(readings)
      assert reading.processor_id == processor.id
      assert reading.load_percent == 25.0
    end

    test "handles SNMP errors gracefully", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Error Device",
          ip_address: "192.168.1.51",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true
        })

      {:ok, snmp_device} = Repo.insert(%Device{device_id: device.id})

      {:ok, _sensor} =
        Repo.insert(%Sensor{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1",
          sensor_descr: "Chassis Temp"
        })

      # Mock timeout
      SnmpMock
      |> expect(:get, fn _target, "1.3.6.1.2.1.99.1.1.1.4.1", _opts ->
        {:error, :timeout}
      end)
      |> stub(:walk, fn _, _, _ -> {:ok, []} end)
      |> stub(:get, fn _, _, _ -> {:error, :no_such_object} end)

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

  describe "start_polling/1" do
    test "schedules initial job with offset", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          check_interval_seconds: 300
        })

      # Get the expected offset
      expected_offset = PollingOffset.calculate_offset(device.id, 300)

      # Start polling
      assert {:ok, job} = DevicePollerWorker.start_polling(device.id)

      # Verify job is scheduled with offset
      assert job.args["device_id"] == device.id
      assert job.scheduled_at

      # Calculate the delay (scheduled_at - inserted_at)
      delay_seconds = DateTime.diff(job.scheduled_at, job.inserted_at, :second)
      assert delay_seconds == expected_offset
    end

    test "schedules initial job with offset for different interval", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: site.organization_id,
          check_interval_seconds: 600
        })

      # Get the expected offset
      expected_offset = PollingOffset.calculate_offset(device.id, 600)

      # Start polling
      assert {:ok, job} = DevicePollerWorker.start_polling(device.id)

      # Verify job is scheduled with offset
      delay_seconds = DateTime.diff(job.scheduled_at, job.inserted_at, :second)
      assert delay_seconds == expected_offset
    end
  end

  describe "stop_polling/1" do
    test "cancels all jobs for device", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router 3",
          ip_address: "192.168.1.3",
          site_id: site.id,
          organization_id: site.organization_id
        })

      # Start polling
      assert {:ok, _job} = DevicePollerWorker.start_polling(device.id)

      # Stop polling
      assert {:ok, cancelled_jobs} = DevicePollerWorker.stop_polling(device.id)
      refute cancelled_jobs == []
    end
  end

  describe "race condition handling" do
    test "handles device deletion during poll gracefully", %{organization: org, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Test Device",
          ip_address: "192.168.1.100",
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          site_id: site.id,
          organization_id: org.id
        })

      # Perform polling on device without SNMP setup (no sensors/interfaces)
      # Should complete without crashing
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})

      # Delete device
      Devices.delete_device(device)

      # Verify polling with non-existent device doesn't crash
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
    end

    test "discards poll results when device is reassigned to agent", %{
      organization: org,
      site: site
    } do
      # Create a local agent token (is_cloud_poller defaults to false)
      {:ok, agent_token, _token} = Towerops.Agents.create_agent_token(org.id, "Local Agent")

      {:ok, device} =
        Devices.create_device(%{
          name: "Test Device",
          ip_address: "192.168.1.100",
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          site_id: site.id,
          organization_id: org.id
        })

      # Create SNMP device
      snmp_device =
        %Device{}
        |> Device.changeset(%{
          device_id: device.id,
          sys_name: "test-device",
          sys_descr: "Test Device"
        })
        |> Repo.insert!()

      # Create a sensor
      %Sensor{}
      |> Sensor.changeset(%{
        snmp_device_id: snmp_device.id,
        sensor_type: "temperature",
        sensor_index: "1",
        sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1",
        sensor_descr: "Test Sensor",
        sensor_unit: "C",
        sensor_divisor: 1,
        current_reading: 25.0
      })
      |> Repo.insert!()

      # Initial state: Phoenix should poll (no agent assigned)
      assert Towerops.Agents.should_phoenix_poll_device?(device)

      # Mock SNMP to simulate slow polling
      poll_started = :erlang.monotonic_time(:millisecond)

      expect(SnmpMock, :get, fn _, _, _ ->
        # Simulate device reassignment during poll
        if :erlang.monotonic_time(:millisecond) - poll_started < 100 do
          # First call - assign to agent
          {:ok, _} = Towerops.Agents.assign_device_to_agent(agent_token.id, device.id)
          Process.sleep(50)
        end

        {:ok, 30}
      end)

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

      # Perform polling - should detect reassignment and discard results
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})

      # Verify device is now assigned to local agent
      device = Repo.reload!(device)
      refute Towerops.Agents.should_phoenix_poll_device?(device)
    end

    test "uses fresh poll interval when rescheduling after config change", %{
      organization: org,
      site: site
    } do
      {:ok, device} =
        Devices.create_device(%{
          name: "Test Device",
          ip_address: "192.168.1.100",
          snmp_enabled: true,
          check_interval_seconds: 60,
          site_id: site.id,
          organization_id: org.id
        })

      # Change interval during "polling" (before scheduling next poll)
      {:ok, _device} = Devices.update_device(device, %{check_interval_seconds: 300})

      # Perform polling - should use NEW interval (300s) when scheduling next poll
      # Device has no SNMP setup, so it won't actually poll but will reschedule
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})

      # The code path re-fetches the device for fresh config before rescheduling
    end

    test "stops rescheduling when device is deleted", %{organization: org, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Test Device",
          ip_address: "192.168.1.100",
          snmp_enabled: true,
          site_id: site.id,
          organization_id: org.id
        })

      # Perform polling
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})

      # Delete device
      Devices.delete_device(device)

      # Perform polling again - should not crash and not reschedule
      assert :ok = DevicePollerWorker.perform(%Oban.Job{args: %{"device_id" => device.id}})
    end
  end
end
