defmodule Towerops.SnmpTest do
  use Towerops.DataCase

  import Towerops.AccountsFixtures

  alias Towerops.Snmp
  alias Towerops.Snmp.Device
  alias Towerops.Snmp.Interface
  alias Towerops.Snmp.InterfaceStat
  alias Towerops.Snmp.Sensor
  alias Towerops.Snmp.SensorReading
  alias Towerops.Snmp.SnmpMock

  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 Router",
        ip_address: "192.168.1.1",
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public",
        snmp_port: 161,
        site_id: site.id
      })

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

    %{device: device, snmp_device: snmp_device, organization: organization}
  end

  describe "test_connection/4" do
    import Mox

    setup :verify_on_exit!

    test "delegates to Client.test_connection" do
      expect(SnmpMock, :get, fn _, _, _ ->
        {:ok, 12_345}
      end)

      result = Snmp.test_connection("192.168.1.1", "public", "2c")

      case result do
        {:ok, _} -> assert true
        {:error, _} -> assert true
      end
    end

    test "accepts custom port" do
      expect(SnmpMock, :get, fn _, _, _ ->
        {:ok, 12_345}
      end)

      result = Snmp.test_connection("192.168.1.1", "public", "2c", 1161)

      case result do
        {:ok, _} -> assert true
        {:error, _} -> assert true
      end
    end
  end

  describe "get_device/1" do
    test "returns device for device_id", %{device: device, snmp_device: snmp_device} do
      found_device = Snmp.get_device(device.id)
      assert found_device.id == snmp_device.id
      assert found_device.sys_name == "test-router"
    end

    test "returns nil for non-existent device_id" do
      assert Snmp.get_device(Ecto.UUID.generate()) == nil
    end
  end

  describe "get_device_with_associations/1" do
    test "returns device with preloaded associations", %{device: device, snmp_device: snmp_device} do
      found_device = Snmp.get_device_with_associations(device.id)
      assert found_device.id == snmp_device.id
      assert Ecto.assoc_loaded?(found_device.interfaces)
      assert Ecto.assoc_loaded?(found_device.sensors)
    end

    test "returns nil for non-existent device_id" do
      assert Snmp.get_device_with_associations(Ecto.UUID.generate()) == nil
    end
  end

  describe "list_interfaces/1" do
    test "returns all interfaces for a device ordered by if_index", %{device: device, snmp_device: snmp_device} do
      interface1 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_name: "eth1"
        })
        |> Repo.insert!()

      interface2 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      interfaces = Snmp.list_interfaces(snmp_device.id)
      assert length(interfaces) == 2
      assert hd(interfaces).id == interface2.id
      assert List.last(interfaces).id == interface1.id
    end

    test "returns empty list for device with no interfaces", %{device: device, snmp_device: snmp_device} do
      assert Snmp.list_interfaces(snmp_device.id) == []
    end
  end

  describe "list_monitored_interfaces/1" do
    test "returns only monitored interfaces", %{device: device, snmp_device: snmp_device} do
      monitored =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0",
          monitored: true
        })
        |> Repo.insert!()

      _unmonitored =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_name: "eth1",
          monitored: false
        })
        |> Repo.insert!()

      interfaces = Snmp.list_monitored_interfaces(snmp_device.id)
      assert length(interfaces) == 1
      assert hd(interfaces).id == monitored.id
    end
  end

  describe "get_interface/1" do
    test "returns interface by id", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      found = Snmp.get_interface(interface.id)
      assert found.id == interface.id
      assert found.if_name == "eth0"
    end

    test "returns nil for non-existent id" do
      assert Snmp.get_interface(Ecto.UUID.generate()) == nil
    end
  end

  describe "update_interface/2" do
    test "updates interface attributes", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0",
          monitored: false
        })
        |> Repo.insert!()

      {:ok, updated} = Snmp.update_interface(interface, %{monitored: true})
      assert updated.monitored == true
    end

    test "returns error for invalid attributes", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      {:error, changeset} = Snmp.update_interface(interface, %{if_index: nil})
      assert changeset.errors[:if_index]
    end
  end

  describe "list_sensors/1" do
    test "returns all sensors for a device ordered by type and index", %{device: device, snmp_device: snmp_device} do
      sensor1 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      sensor2 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "2",
          sensor_oid: "1.2.3.5"
        })
        |> Repo.insert!()

      sensors = Snmp.list_sensors(snmp_device.id)
      assert length(sensors) == 2
      assert sensor1.id in Enum.map(sensors, & &1.id)
      assert sensor2.id in Enum.map(sensors, & &1.id)
    end

    test "returns empty list for device with no sensors", %{device: device, snmp_device: snmp_device} do
      assert Snmp.list_sensors(snmp_device.id) == []
    end
  end

  describe "list_monitored_sensors/1" do
    test "returns only monitored sensors", %{device: device, snmp_device: snmp_device} do
      monitored =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4",
          monitored: true
        })
        |> Repo.insert!()

      _unmonitored =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "2",
          sensor_oid: "1.2.3.5",
          monitored: false
        })
        |> Repo.insert!()

      sensors = Snmp.list_monitored_sensors(snmp_device.id)
      assert length(sensors) == 1
      assert hd(sensors).id == monitored.id
    end
  end

  describe "list_sensors_by_type/1" do
    test "groups sensors by type", %{device: device, snmp_device: snmp_device} do
      _temp1 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      _temp2 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "2",
          sensor_oid: "1.2.3.5"
        })
        |> Repo.insert!()

      _power =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "3",
          sensor_oid: "1.2.3.6"
        })
        |> Repo.insert!()

      grouped = Snmp.list_sensors_by_type(snmp_device.id)
      assert Map.has_key?(grouped, "temperature")
      assert Map.has_key?(grouped, "power")
      assert length(grouped["temperature"]) == 2
      assert length(grouped["power"]) == 1
    end

    test "returns empty map for device with no sensors", %{device: device, snmp_device: snmp_device} do
      assert Snmp.list_sensors_by_type(snmp_device.id) == %{}
    end
  end

  describe "get_sensor/1" do
    test "returns sensor by id", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      found = Snmp.get_sensor(sensor.id)
      assert found.id == sensor.id
      assert found.sensor_type == "temperature"
    end

    test "returns nil for non-existent id" do
      assert Snmp.get_sensor(Ecto.UUID.generate()) == nil
    end
  end

  describe "update_sensor/2" do
    test "updates sensor attributes", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4",
          monitored: false
        })
        |> Repo.insert!()

      {:ok, updated} = Snmp.update_sensor(sensor, %{monitored: true})
      assert updated.monitored == true
    end

    test "returns error for invalid attributes", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      {:error, changeset} = Snmp.update_sensor(sensor, %{sensor_oid: nil})
      assert changeset.errors[:sensor_oid]
    end
  end

  describe "get_sensor_readings/2" do
    test "returns recent readings for a sensor", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      for i <- 1..5 do
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 20.0 + i,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      readings = Snmp.get_sensor_readings(sensor.id)
      assert length(readings) == 5
    end

    test "respects limit option", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      for i <- 1..10 do
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 20.0 + i,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      readings = Snmp.get_sensor_readings(sensor.id, limit: 3)
      assert length(readings) == 3
    end

    test "respects since option", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      past = DateTime.add(DateTime.utc_now(), -3600, :second)
      recent = DateTime.add(DateTime.utc_now(), -60, :second)

      %SensorReading{}
      |> SensorReading.changeset(%{
        sensor_id: sensor.id,
        value: 20.0,
        status: "ok",
        checked_at: past
      })
      |> Repo.insert!()

      %SensorReading{}
      |> SensorReading.changeset(%{
        sensor_id: sensor.id,
        value: 25.0,
        status: "ok",
        checked_at: recent
      })
      |> Repo.insert!()

      since_time = DateTime.add(DateTime.utc_now(), -120, :second)
      readings = Snmp.get_sensor_readings(sensor.id, since: since_time)
      assert length(readings) == 1
      assert hd(readings).value == 25.0
    end

    test "returns empty list for sensor with no readings", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      assert Snmp.get_sensor_readings(sensor.id) == []
    end
  end

  describe "get_latest_sensor_reading/1" do
    test "returns most recent reading", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      old_reading =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 20.0,
          status: "ok",
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_reading =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 25.0,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      latest = Snmp.get_latest_sensor_reading(sensor.id)
      assert latest.id == new_reading.id
      refute latest.id == old_reading.id
    end

    test "returns nil for sensor with no readings", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      assert Snmp.get_latest_sensor_reading(sensor.id) == nil
    end
  end

  describe "get_interface_stats/2" do
    test "returns recent stats for an interface", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      for i <- 1..5 do
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: i * 1000,
          if_out_octets: i * 2000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      stats = Snmp.get_interface_stats(interface.id)
      assert length(stats) == 5
    end

    test "respects limit option", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      for i <- 1..10 do
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: i * 1000,
          if_out_octets: i * 2000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      stats = Snmp.get_interface_stats(interface.id, limit: 3)
      assert length(stats) == 3
    end

    test "respects since option", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      past = DateTime.add(DateTime.utc_now(), -3600, :second)
      recent = DateTime.add(DateTime.utc_now(), -60, :second)

      %InterfaceStat{}
      |> InterfaceStat.changeset(%{
        interface_id: interface.id,
        if_in_octets: 1000,
        if_out_octets: 2000,
        checked_at: past
      })
      |> Repo.insert!()

      %InterfaceStat{}
      |> InterfaceStat.changeset(%{
        interface_id: interface.id,
        if_in_octets: 5000,
        if_out_octets: 6000,
        checked_at: recent
      })
      |> Repo.insert!()

      since_time = DateTime.add(DateTime.utc_now(), -120, :second)
      stats = Snmp.get_interface_stats(interface.id, since: since_time)
      assert length(stats) == 1
      assert hd(stats).if_in_octets == 5000
    end

    test "returns empty list for interface with no stats", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      assert Snmp.get_interface_stats(interface.id) == []
    end
  end

  describe "get_latest_interface_stat/1" do
    test "returns most recent stat", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      old_stat =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: 1000,
          if_out_octets: 2000,
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_stat =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: 5000,
          if_out_octets: 6000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      latest = Snmp.get_latest_interface_stat(interface.id)
      assert latest.id == new_stat.id
      refute latest.id == old_stat.id
    end

    test "returns nil for interface with no stats", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      assert Snmp.get_latest_interface_stat(interface.id) == nil
    end
  end

  describe "create_sensor_reading/1" do
    test "creates a sensor reading with valid attributes", %{device: device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      {:ok, reading} =
        Snmp.create_sensor_reading(%{
          sensor_id: sensor.id,
          value: 25.5,
          status: "ok",
          checked_at: DateTime.utc_now()
        })

      assert reading.sensor_id == sensor.id
      assert reading.value == 25.5
      assert reading.status == "ok"
    end

    test "returns error with invalid attributes" do
      {:error, changeset} = Snmp.create_sensor_reading(%{value: "not a number"})
      assert changeset.errors[:sensor_id]
    end
  end

  describe "create_interface_stat/1" do
    test "creates an interface stat with valid attributes", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      {:ok, stat} =
        Snmp.create_interface_stat(%{
          interface_id: interface.id,
          if_in_octets: 1000,
          if_out_octets: 2000,
          if_in_errors: 0,
          if_out_errors: 0,
          if_in_discards: 0,
          if_out_discards: 0,
          checked_at: DateTime.utc_now()
        })

      assert stat.interface_id == interface.id
      assert stat.if_in_octets == 1000
      assert stat.if_out_octets == 2000
    end

    test "returns error with invalid attributes" do
      {:error, changeset} = Snmp.create_interface_stat(%{if_in_octets: "not a number"})
      assert changeset.errors[:interface_id]
    end
  end
end
