defmodule Towerops.Workers.AgentLatencyEvaluatorTest do
  use Towerops.DataCase, async: false
  use Oban.Testing, repo: Towerops.Repo

  import Towerops.AccountsFixtures
  import Towerops.OrganizationsFixtures

  alias Towerops.Agents
  alias Towerops.Agents.Stats
  alias Towerops.Monitoring
  alias Towerops.Repo
  alias Towerops.Sites
  alias Towerops.Workers.AgentLatencyEvaluator

  describe "perform/1" do
    test "returns :ok when no reassignment candidates exist" do
      assert :ok = perform_job(AgentLatencyEvaluator, %{})
    end

    test "reassigns device to faster cloud agent when 20%+ improvement available" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      # Create checks: slow = 100ms, fast = 50ms (50% improvement)
      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_cloud.id,
          status: "success",
          response_time_ms: 100,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_cloud.id,
          status: "success",
          response_time_ms: 50,
          checked_at: DateTime.utc_now()
        })
      end

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      # Device-level assignment should be created to fast cloud agent
      assignment = Agents.get_device_assignment(device.id)
      assert assignment.agent_token_id == fast_cloud.id
    end

    test "does not reassign when improvement is below 20% threshold" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, cloud1, _} = Agents.create_cloud_poller("Cloud 1")
      {:ok, cloud2, _} = Agents.create_cloud_poller("Cloud 2")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      # Create checks: cloud1 = 100ms, cloud2 = 85ms (15% improvement, below 20%)
      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: cloud1.id,
          status: "success",
          response_time_ms: 100,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: cloud2.id,
          status: "success",
          response_time_ms: 85,
          checked_at: DateTime.utc_now()
        })
      end

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      # No device assignment should be created
      assert Agents.get_device_assignment(device.id) == nil
    end

    test "reassigns organization-level cloud poller to device-level" do
      user = user_fixture()

      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

      org =
        organization_fixture(user.id, %{
          default_agent_token_id: slow_cloud.id
        })

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_cloud.id,
          status: "success",
          response_time_ms: 150,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_cloud.id,
          status: "success",
          response_time_ms: 50,
          checked_at: DateTime.utc_now()
        })
      end

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      # Should create device-level assignment, not change org default
      assignment = Agents.get_device_assignment(device.id)
      assert assignment.agent_token_id == fast_cloud.id

      # Organization default should remain unchanged
      updated_org = Towerops.Organizations.get_organization!(org.id)
      assert updated_org.default_agent_token_id == slow_cloud.id
    end

    test "creates device assignment for global default assignment" do
      user = user_fixture()
      org = organization_fixture(user.id)

      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Global")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Global")

      {:ok, _} = Towerops.Settings.set_global_default_cloud_poller(slow_cloud.id)

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_cloud.id,
          status: "success",
          response_time_ms: 200,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_cloud.id,
          status: "success",
          response_time_ms: 60,
          checked_at: DateTime.utc_now()
        })
      end

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      assignment = Agents.get_device_assignment(device.id)
      assert assignment.agent_token_id == fast_cloud.id

      {:ok, _} = Towerops.Settings.set_global_default_cloud_poller(nil)
    end

    test "creates device assignment for cloud polling (no assignment)" do
      user = user_fixture()
      org = organization_fixture(user.id)

      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      now = DateTime.truncate(DateTime.utc_now(), :second)

      for_result =
        for _ <- 1..10 do
          [
            %{
              id: Ecto.UUID.generate(),
              device_id: device.id,
              agent_token_id: slow_cloud.id,
              status: "success",
              response_time_ms: 100.0,
              checked_at: now,
              inserted_at: now
            },
            %{
              id: Ecto.UUID.generate(),
              device_id: device.id,
              agent_token_id: fast_cloud.id,
              status: "success",
              response_time_ms: 50.0,
              checked_at: now,
              inserted_at: now
            }
          ]
        end

      checks = List.flatten(for_result)

      Repo.insert_all(Towerops.Monitoring.MonitoringCheck, checks)

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      assignment = Agents.get_device_assignment(device.id)
      assert assignment != nil, "Expected device assignment to be created"
      assert assignment.agent_token_id == fast_cloud.id
    end

    test "does not reassign direct device assignments" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      # Create direct device assignment
      {:ok, _} = Agents.assign_device_to_agent(slow_cloud.id, device.id)

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_cloud.id,
          status: "success",
          response_time_ms: 100,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_cloud.id,
          status: "success",
          response_time_ms: 50,
          checked_at: DateTime.utc_now()
        })
      end

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      # Device assignment should remain with slow cloud (direct assignment not changed)
      assignment = Agents.get_device_assignment(device.id)
      assert assignment.agent_token_id == slow_cloud.id
    end

    test "requires minimum check count" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      # Create only 3 checks (below minimum of 5)
      for _ <- 1..3 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_cloud.id,
          status: "success",
          response_time_ms: 100,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_cloud.id,
          status: "success",
          response_time_ms: 50,
          checked_at: DateTime.utc_now()
        })
      end

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      # No assignment created (insufficient data)
      assert Agents.get_device_assignment(device.id) == nil
    end

    test "handles multiple devices in single evaluation" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

      {:ok, site1} =
        Sites.create_site(%{
          name: "Site 1",
          organization_id: org.id,
          agent_token_id: slow_cloud.id
        })

      {:ok, site2} =
        Sites.create_site(%{
          name: "Site 2",
          organization_id: org.id,
          agent_token_id: slow_cloud.id
        })

      {:ok, device1} =
        Towerops.Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site1.id,
          organization_id: site1.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      {:ok, device2} =
        Towerops.Devices.create_device(%{
          name: "Router 2",
          ip_address: "192.168.2.1",
          site_id: site2.id,
          organization_id: site2.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      for device_id <- [device1.id, device2.id] do
        for _ <- 1..10 do
          Monitoring.create_monitoring_check(%{
            device_id: device_id,
            agent_token_id: slow_cloud.id,
            status: "success",
            response_time_ms: 100,
            checked_at: DateTime.utc_now()
          })

          Monitoring.create_monitoring_check(%{
            device_id: device_id,
            agent_token_id: fast_cloud.id,
            status: "success",
            response_time_ms: 50,
            checked_at: DateTime.utc_now()
          })
        end
      end

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      # Both devices should get device-level assignments
      assignment1 = Agents.get_device_assignment(device1.id)
      assignment2 = Agents.get_device_assignment(device2.id)

      assert assignment1.agent_token_id == fast_cloud.id
      assert assignment2.agent_token_id == fast_cloud.id
    end

    test "broadcasts PubSub event when device is reassigned" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_cloud.id,
          status: "success",
          response_time_ms: 100,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_cloud.id,
          status: "success",
          response_time_ms: 50,
          checked_at: DateTime.utc_now()
        })
      end

      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:latency")

      assert :ok = perform_job(AgentLatencyEvaluator, %{})

      assert_received {:device_reassigned, candidate}
      assert candidate.device_id == device.id
      assert candidate.best_agent_token_id == fast_cloud.id
      assert candidate.improvement_percent >= 20
    end

    test "does not broadcast when no reassignment occurs" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, cloud1, _} = Agents.create_cloud_poller("Cloud 1")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      # Only one cloud agent - no alternative to compare
      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: cloud1.id,
          status: "success",
          response_time_ms: 100,
          checked_at: DateTime.utc_now()
        })
      end

      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:latency")

      assert :ok = perform_job(AgentLatencyEvaluator, %{})

      refute_received {:device_reassigned, _}
    end

    test "handles reassignment failures gracefully" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, slow_cloud, _} = Agents.create_cloud_poller("Slow Cloud")
      {:ok, fast_cloud, _} = Agents.create_cloud_poller("Fast Cloud")

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

      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: site.organization_id,
          snmp_enabled: true,
          snmp_community: "public",
          snmp_version: "2c"
        })

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_cloud.id,
          status: "success",
          response_time_ms: 100,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_cloud.id,
          status: "success",
          response_time_ms: 50,
          checked_at: DateTime.utc_now()
        })
      end

      # Delete the device to cause a failure during reassignment
      Towerops.Devices.delete_device(device)

      # Worker should complete successfully despite the error
      assert :ok = perform_job(AgentLatencyEvaluator, %{})
    end

    test "ignores local agent latency data" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, slow_local, _} = Agents.create_agent_token(org.id, "Slow Local")
      {:ok, fast_local, _} = Agents.create_agent_token(org.id, "Fast Local")

      {:ok, site} =
        Sites.create_site(%{
          name: "Local Site",
          organization_id: org.id,
          agent_token_id: slow_local.id
        })

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

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: slow_local.id,
          status: "success",
          response_time_ms: 100.0,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: fast_local.id,
          status: "success",
          response_time_ms: 30.0,
          checked_at: DateTime.utc_now()
        })
      end

      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:latency")

      assert :ok = AgentLatencyEvaluator.perform(%Oban.Job{args: %{}})

      # Local agents should not trigger reassignment
      refute_receive {:device_reassigned, _}, 200
    end
  end

  describe "find_reassignment_candidates with cloud_only" do
    test "filters to cloud pollers only" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, cloud1, _} = Agents.create_cloud_poller("Cloud 1")
      {:ok, cloud2, _} = Agents.create_cloud_poller("Cloud 2")
      {:ok, local, _} = Agents.create_agent_token(org.id, "Local Agent")

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

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

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: cloud1.id,
          status: "success",
          response_time_ms: 100.0,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: cloud2.id,
          status: "success",
          response_time_ms: 30.0,
          checked_at: DateTime.utc_now()
        })

        # Local agent has best latency but should be excluded
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: local.id,
          status: "success",
          response_time_ms: 10.0,
          checked_at: DateTime.utc_now()
        })
      end

      candidates =
        Stats.find_reassignment_candidates(
          cloud_only: true,
          min_checks_per_agent: 5,
          hours_ago: 48
        )

      assert length(candidates) == 1
      candidate = hd(candidates)
      # Should pick cloud2 as best, not local agent
      assert candidate.best_agent_token_id == cloud2.id
    end

    test "returns no candidates when cloud_only and only local agents" do
      user = user_fixture()
      org = organization_fixture(user.id)
      {:ok, local1, _} = Agents.create_agent_token(org.id, "Local 1")
      {:ok, local2, _} = Agents.create_agent_token(org.id, "Local 2")

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

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

      for _ <- 1..10 do
        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: local1.id,
          status: "success",
          response_time_ms: 100.0,
          checked_at: DateTime.utc_now()
        })

        Monitoring.create_monitoring_check(%{
          device_id: device.id,
          agent_token_id: local2.id,
          status: "success",
          response_time_ms: 30.0,
          checked_at: DateTime.utc_now()
        })
      end

      candidates =
        Stats.find_reassignment_candidates(
          cloud_only: true,
          min_checks_per_agent: 5,
          hours_ago: 48
        )

      assert candidates == []
    end
  end
end
