defmodule Towerops.Preseem.BaselineTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures
  import Towerops.DevicesFixtures
  import Towerops.OrganizationsFixtures

  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.Baseline
  alias Towerops.Preseem.DeviceBaseline
  alias Towerops.Preseem.SubscriberMetric

  setup do
    user = user_fixture()
    org = organization_fixture(user.id)
    %{org: org}
  end

  describe "compute_baselines/1" do
    test "computes baselines for matched APs with sufficient data", %{org: org} do
      ap = insert_matched_ap(org)

      for i <- 1..5 do
        insert_subscriber_metric(ap.id, %{
          avg_latency: 10.0 + i,
          p95_latency: 20.0 + i,
          subscriber_count: 40 + i,
          recorded_at: hours_ago(i)
        })
      end

      assert {:ok, 1} = Baseline.compute_baselines(org.id)

      baselines = Repo.all(DeviceBaseline)
      refute baselines == []

      latency_baseline = Enum.find(baselines, &(&1.metric_name == "avg_latency"))
      assert latency_baseline
      assert latency_baseline.sample_count == 5
      assert latency_baseline.period == "all"
      assert latency_baseline.mean
      assert latency_baseline.stddev
      assert latency_baseline.p5
      assert latency_baseline.p95
    end

    test "skips APs with fewer than 3 data points", %{org: org} do
      ap = insert_matched_ap(org)

      for i <- 1..2 do
        insert_subscriber_metric(ap.id, %{
          avg_latency: 10.0 + i,
          recorded_at: hours_ago(i)
        })
      end

      assert {:ok, 0} = Baseline.compute_baselines(org.id)
      assert Repo.all(DeviceBaseline) == []
    end

    test "skips unmatched APs", %{org: org} do
      ap = insert_unmatched_ap(org.id)

      for i <- 1..5 do
        insert_subscriber_metric(ap.id, %{
          avg_latency: 10.0 + i,
          recorded_at: hours_ago(i)
        })
      end

      assert {:ok, 0} = Baseline.compute_baselines(org.id)
    end

    test "handles empty organization", %{org: org} do
      assert {:ok, 0} = Baseline.compute_baselines(org.id)
    end

    test "only includes metrics within the 14-day window", %{org: org} do
      ap = insert_matched_ap(org)

      # 3 recent metrics (within window)
      for i <- 1..3 do
        insert_subscriber_metric(ap.id, %{
          avg_latency: 10.0 + i,
          recorded_at: hours_ago(i)
        })
      end

      # 2 old metrics (outside 14-day window)
      for i <- 1..2 do
        insert_subscriber_metric(ap.id, %{
          avg_latency: 100.0 + i,
          recorded_at: days_ago(15 + i)
        })
      end

      assert {:ok, 1} = Baseline.compute_baselines(org.id)

      latency_baseline =
        DeviceBaseline
        |> Repo.all()
        |> Enum.find(&(&1.metric_name == "avg_latency"))

      # Should only have 3 samples from the recent window
      assert latency_baseline.sample_count == 3
    end

    test "upserts baselines on recomputation", %{org: org} do
      ap = insert_matched_ap(org)

      for i <- 1..5 do
        insert_subscriber_metric(ap.id, %{
          avg_latency: 10.0 + i,
          recorded_at: hours_ago(i)
        })
      end

      assert {:ok, 1} = Baseline.compute_baselines(org.id)
      initial_count = length(Repo.all(DeviceBaseline))

      # Recompute - should update existing baselines, not create duplicates
      assert {:ok, 1} = Baseline.compute_baselines(org.id)
      assert length(Repo.all(DeviceBaseline)) == initial_count
    end

    test "computes baselines for multiple matched APs", %{org: org} do
      for _idx <- 1..3 do
        ap = insert_matched_ap(org)

        for i <- 1..4 do
          insert_subscriber_metric(ap.id, %{
            avg_latency: 10.0 + i,
            recorded_at: hours_ago(i)
          })
        end
      end

      assert {:ok, 3} = Baseline.compute_baselines(org.id)
    end

    test "skips metrics with nil values for a given metric name", %{org: org} do
      ap = insert_matched_ap(org)

      # Insert metrics where avg_jitter is nil but avg_latency has values
      for i <- 1..5 do
        insert_subscriber_metric(ap.id, %{
          avg_latency: 10.0 + i,
          avg_jitter: nil,
          recorded_at: hours_ago(i)
        })
      end

      assert {:ok, 1} = Baseline.compute_baselines(org.id)

      baselines = Repo.all(DeviceBaseline)
      latency_baseline = Enum.find(baselines, &(&1.metric_name == "avg_latency"))
      jitter_baseline = Enum.find(baselines, &(&1.metric_name == "avg_jitter"))

      assert latency_baseline
      # avg_jitter should not have a baseline since all values are nil (< 3 non-nil)
      assert jitter_baseline == nil
    end
  end

  # Helpers

  defp insert_matched_ap(org) do
    device = device_fixture(%{organization_id: org.id})

    %AccessPoint{}
    |> AccessPoint.changeset(%{
      organization_id: org.id,
      preseem_id: "ap-#{System.unique_integer([:positive])}",
      name: "Test AP",
      device_id: device.id,
      match_confidence: "auto_ip"
    })
    |> Repo.insert!()
  end

  defp insert_unmatched_ap(org_id) do
    %AccessPoint{}
    |> AccessPoint.changeset(%{
      organization_id: org_id,
      preseem_id: "ap-unmatched-#{System.unique_integer([:positive])}",
      name: "Unmatched AP"
    })
    |> Repo.insert!()
  end

  defp insert_subscriber_metric(ap_id, attrs) do
    %SubscriberMetric{}
    |> SubscriberMetric.changeset(Map.put(attrs, :preseem_access_point_id, ap_id))
    |> Repo.insert!()
  end

  defp hours_ago(n) do
    DateTime.utc_now() |> DateTime.add(-n * 3600, :second) |> DateTime.truncate(:second)
  end

  defp days_ago(n) do
    DateTime.utc_now() |> DateTime.add(-n * 86_400, :second) |> DateTime.truncate(:second)
  end
end
