defmodule Towerops.Monitoring.DeviceMonitorTest do
  use Towerops.DataCase, async: false

  import Mox
  import Towerops.AccountsFixtures

  alias Towerops.Alerts
  alias Towerops.Monitoring
  alias Towerops.Monitoring.DeviceMonitor
  alias Towerops.Monitoring.PingMock

  # Set up Mox to verify expectations on exit
  setup :verify_on_exit!
  setup :set_mox_from_context

  setup do
    # Stub default ping behavior using the PingStub module
    Mox.stub_with(PingMock, Towerops.Monitoring.PingStub)

    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: "Router 1",
        ip_address: "127.0.0.1",
        site_id: site.id,
        monitoring_enabled: true,
        check_interval_seconds: 1
      })

    %{device: device}
  end

  describe "init/1" do
    test "initializes with device_id and schedules check if monitoring enabled", %{
      device: device
    } do
      {:ok, state} = DeviceMonitor.init(device.id)
      assert state.device_id == device.id
    end

    test "initializes but doesn't schedule if monitoring disabled", %{device: device} do
      {:ok, updated} = Towerops.Devices.update_device(device, %{monitoring_enabled: false})
      {:ok, state} = DeviceMonitor.init(updated.id)
      assert state.device_id == updated.id
    end
  end

  describe "trigger_check/1" do
    test "can trigger an immediate check", %{device: device} do
      # Start the monitor
      start_supervised!({DeviceMonitor, device.id})

      # Trigger a check
      DeviceMonitor.trigger_check(device.id)

      # Give it time to process (GenServer needs time to handle the message)
      Process.sleep(200)

      # Should have created a monitoring check
      checks = Monitoring.list_devices_checks(device.id)
      refute Enum.empty?(checks)
    end
  end

  describe "handle_info :check_device" do
    test "performs check and creates monitoring record", %{device: device} do
      {:ok, state} = DeviceMonitor.init(device.id)

      # Simulate the periodic check message
      {:noreply, _new_state} = DeviceMonitor.handle_info(:check_device, state)

      # Should have created a check
      checks = Monitoring.list_devices_checks(device.id)
      assert length(checks) == 1
    end
  end

  describe "SNMP monitoring configuration" do
    test "equipment can be configured with SNMP settings", %{device: device} do
      {:ok, updated_device} =
        Towerops.Devices.update_device(device, %{
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_community: "public",
          snmp_port: 161
        })

      assert updated_device.snmp_enabled == true
      assert updated_device.snmp_version == "2c"
      assert updated_device.snmp_community == "public"
      assert updated_device.snmp_port == 161
    end

    test "SNMP version defaults to 2c", %{device: device} do
      {:ok, updated_device} =
        Towerops.Devices.update_device(device, %{
          snmp_enabled: true,
          snmp_community: "public"
        })

      assert updated_device.snmp_version == "2c"
    end
  end

  describe "alert creation" do
    setup %{device: device} do
      # Set equipment to up status first
      {:ok, device} = Towerops.Devices.update_device_status(device, :up)
      %{device: device}
    end

    test "creates device_down alert when equipment goes down", %{device: device} do
      # Mock failed ping
      expect(PingMock, :ping, fn _ip ->
        {:error, :timeout_or_unreachable}
      end)

      {:ok, state} = DeviceMonitor.init(device.id)

      # Trigger check
      {:noreply, _state} = DeviceMonitor.handle_info(:check_device, state)

      # Should create an alert (no need to sleep since test env doesn't send emails)
      alerts = Alerts.list_devices_alerts(device.id)
      refute Enum.empty?(alerts)

      alert = hd(alerts)
      assert alert.alert_type == :device_down
    end

    test "creates device_up alert when equipment recovers", %{device: device} do
      # First set to down
      {:ok, device} = Towerops.Devices.update_device_status(device, :down)

      # Create a down alert
      {:ok, _} =
        Alerts.create_alert(%{
          device_id: device.id,
          alert_type: :device_down,
          triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
          message: "Equipment is down"
        })

      {:ok, state} = DeviceMonitor.init(device.id)

      # Trigger check
      {:noreply, _state} = DeviceMonitor.handle_info(:check_device, state)

      # Should create recovery alert
      alerts = Alerts.list_devices_alerts(device.id, 10)
      up_alerts = Enum.filter(alerts, &(&1.alert_type == :device_up))
      refute Enum.empty?(up_alerts)
    end

    test "does not create duplicate down alerts", %{device: device} do
      # Mock failed ping for both calls
      expect(PingMock, :ping, 2, fn _ip ->
        {:error, :timeout_or_unreachable}
      end)

      {:ok, state} = DeviceMonitor.init(device.id)

      # Trigger check twice
      {:noreply, _} = DeviceMonitor.handle_info(:check_device, state)
      {:noreply, _} = DeviceMonitor.handle_info(:check_device, state)

      # Should only have one alert
      alerts =
        device.id
        |> Alerts.list_devices_alerts()
        |> Enum.filter(&(&1.alert_type == :device_down && is_nil(&1.resolved_at)))

      assert length(alerts) == 1
    end

    test "resolves down alert when equipment comes back up", %{device: device} do
      # Set to down first
      {:ok, device} = Towerops.Devices.update_device_status(device, :down)

      # Create down alert
      {:ok, down_alert} =
        Alerts.create_alert(%{
          device_id: device.id,
          alert_type: :device_down,
          triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
          message: "Down"
        })

      {:ok, state} = DeviceMonitor.init(device.id)

      # Check
      {:noreply, _} = DeviceMonitor.handle_info(:check_device, state)

      # Down alert should be resolved
      updated_alert = Alerts.get_alert!(down_alert.id)
      assert updated_alert.resolved_at
    end
  end
end
