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

  import Towerops.AccountsFixtures

  alias Towerops.Devices
  alias Towerops.Organizations
  alias Towerops.Repo
  alias Towerops.Sites
  alias Towerops.Snmp.Device, as: SnmpDevice
  alias Towerops.Snmp.Sensor
  alias Towerops.Snmp.SensorChangeDetector

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

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

    {:ok, snmp_device} =
      Repo.insert(%SnmpDevice{
        device_id: device.id,
        sys_name: "test-device",
        sys_descr: "Test Device"
      })

    %{device: device, snmp_device: snmp_device}
  end

  defp create_sensor(snmp_device, attrs) do
    defaults = %{
      snmp_device_id: snmp_device.id,
      sensor_type: "frequency",
      sensor_index: "1",
      sensor_oid: "1.3.6.1.2.1.99.1.1.1.4.1",
      sensor_descr: "Power Supply 1 Frequency",
      sensor_unit: "Hz",
      sensor_divisor: 1,
      monitored: true,
      metadata: %{}
    }

    {:ok, sensor} = Repo.insert(struct(Sensor, Map.merge(defaults, attrs)))
    sensor
  end

  describe "detect_and_broadcast/3" do
    test "emits sensor_value_changed for non-% sensor when value changes", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor = create_sensor(snmp_device, %{last_value: 50.0, sensor_unit: "Hz"})
      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 49.9, timestamp)

      assert_receive {:device_event, event}
      assert event.event_type == "sensor_value_changed"
      assert event.severity == "info"
      assert event.device_id == device.id
      assert event.metadata.current_value == 49.9
      assert event.metadata.previous_value == 50.0
      assert event.metadata.sensor_id == sensor.id
      assert String.contains?(event.message, "changed from")
      assert String.contains?(event.message, "50.0Hz")
      assert String.contains?(event.message, "49.9Hz")
    end

    test "emits sensor_value_changed for temperature sensor", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor =
        create_sensor(snmp_device, %{
          sensor_type: "temperature",
          sensor_descr: "CPU Temp",
          sensor_unit: "C",
          last_value: 45.0
        })

      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 46.0, timestamp)

      assert_receive {:device_event, event}
      assert event.event_type == "sensor_value_changed"
      assert String.contains?(event.message, "CPU Temp")
      assert String.contains?(event.message, "45.0C")
      assert String.contains?(event.message, "46.0C")
    end

    test "does not emit event when value is unchanged", %{device: device, snmp_device: snmp_device} do
      sensor = create_sensor(snmp_device, %{last_value: 50.0, sensor_unit: "Hz"})
      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 50.0, timestamp)

      refute_receive {:device_event, _}, 100
    end

    test "does not emit sensor_value_changed when last_value is nil", %{device: device, snmp_device: snmp_device} do
      sensor = create_sensor(snmp_device, %{last_value: nil, sensor_unit: "Hz"})
      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 50.0, timestamp)

      refute_receive {:device_event, _}, 100
    end

    test "emits spike event for % sensor with >=30% increase", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor =
        create_sensor(snmp_device, %{
          sensor_type: "percent",
          sensor_descr: "CPU Usage",
          sensor_unit: "%",
          last_value: 20.0,
          metadata: %{}
        })

      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 55.0, timestamp)

      assert_receive {:device_event, event}
      assert event.event_type == "sensor_value_spike"
      assert event.severity == "warning"
    end

    test "emits drop event for % sensor with >=30% decrease", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor =
        create_sensor(snmp_device, %{
          sensor_type: "percent",
          sensor_descr: "CPU Usage",
          sensor_unit: "%",
          last_value: 80.0,
          metadata: %{}
        })

      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 40.0, timestamp)

      assert_receive {:device_event, event}
      assert event.event_type == "sensor_value_drop"
      assert event.severity == "info"
    end

    test "does not emit spike/drop for % sensor with <30% change", %{device: device, snmp_device: snmp_device} do
      sensor =
        create_sensor(snmp_device, %{
          sensor_type: "percent",
          sensor_descr: "CPU Usage",
          sensor_unit: "%",
          last_value: 50.0,
          metadata: %{}
        })

      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 55.0, timestamp)

      # Should not get spike/drop events for small % changes
      # (no sensor_value_changed for % sensors either, they use spike/drop)
      refute_receive {:device_event, _}, 100
    end

    test "emits threshold warning event", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor =
        create_sensor(snmp_device, %{
          sensor_type: "temperature",
          sensor_descr: "Chassis Temp",
          sensor_unit: "C",
          last_value: 60.0,
          metadata: %{"warning_high" => 70, "critical_high" => 90}
        })

      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 75.0, timestamp)

      # Should get both threshold warning AND value changed
      events = collect_events(2)

      event_types = Enum.map(events, & &1.event_type)
      assert "sensor_threshold_warning" in event_types
      assert "sensor_value_changed" in event_types
    end

    test "emits threshold critical event", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor =
        create_sensor(snmp_device, %{
          sensor_type: "temperature",
          sensor_descr: "Chassis Temp",
          sensor_unit: "C",
          last_value: 60.0,
          metadata: %{"warning_high" => 70, "critical_high" => 90}
        })

      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 95.0, timestamp)

      events = collect_events(2)

      event_types = Enum.map(events, & &1.event_type)
      assert "sensor_threshold_critical" in event_types
      assert "sensor_value_changed" in event_types
    end

    test "emits threshold normal event when returning from warning", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor =
        create_sensor(snmp_device, %{
          sensor_type: "temperature",
          sensor_descr: "Chassis Temp",
          sensor_unit: "C",
          last_value: 75.0,
          metadata: %{"warning_high" => 70, "critical_high" => 90}
        })

      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")

      SensorChangeDetector.detect_and_broadcast(sensor, 65.0, timestamp)

      events = collect_events(2)

      event_types = Enum.map(events, & &1.event_type)
      assert "sensor_threshold_normal" in event_types
      assert "sensor_value_changed" in event_types
    end

    test "broadcasts to both device and global event topics", %{
      device: device,
      snmp_device: snmp_device
    } do
      sensor = create_sensor(snmp_device, %{last_value: 50.0, sensor_unit: "Hz"})
      timestamp = DateTime.utc_now()

      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device.id}")
      Phoenix.PubSub.subscribe(Towerops.PubSub, "device:events")

      SensorChangeDetector.detect_and_broadcast(sensor, 49.0, timestamp)

      # Should receive on both topics
      assert_receive {:device_event, event1}
      assert_receive {:device_event, event2}
      assert event1.event_type == "sensor_value_changed"
      assert event2.event_type == "sensor_value_changed"
    end
  end

  # Helper to collect multiple events
  defp collect_events(count) do
    Enum.map(1..count, fn _ ->
      receive do
        {:device_event, event} -> event
      after
        500 -> flunk("Expected #{count} events but timed out")
      end
    end)
  end
end
