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

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

  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.DeviceBaseline
  alias Towerops.Preseem.FleetProfile
  alias Towerops.Preseem.Insight
  alias Towerops.Preseem.Insights

  setup do
    user = user_fixture()
    org = organization_fixture(user.id)
    device = device_fixture(%{organization_id: org.id})

    {:ok, ap} =
      %AccessPoint{}
      |> AccessPoint.changeset(%{
        organization_id: org.id,
        preseem_id: "ap-insights-#{System.unique_integer([:positive])}",
        name: "Insights AP",
        device_id: device.id,
        match_confidence: "auto_ip",
        model: "Ubiquiti AF5XHD",
        qoe_score: 85.0,
        subscriber_count: 30
      })
      |> Repo.insert()

    %{org: org, device: device, access_point: ap}
  end

  describe "list_insights/2" do
    test "returns active insights ordered by urgency then date", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "capacity_saturation", urgency: "info", title: "Info insight"})
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "critical", title: "Critical insight"})
      insert_insight(org, ap, %{type: "model_underperforming", urgency: "warning", title: "Warning insight"})

      insights = Insights.list_insights(org.id)

      assert length(insights) == 3
      assert Enum.at(insights, 0).urgency == "critical"
      assert Enum.at(insights, 1).urgency == "warning"
      assert Enum.at(insights, 2).urgency == "info"
    end

    test "filters by status", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "Active"})
      insert_insight(org, ap, %{type: "capacity_saturation", urgency: "info", title: "Dismissed", status: "dismissed"})

      active = Insights.list_insights(org.id, status: "active")
      assert length(active) == 1
      assert hd(active).title == "Active"

      dismissed = Insights.list_insights(org.id, status: "dismissed")
      assert length(dismissed) == 1
      assert hd(dismissed).title == "Dismissed"
    end

    test "filters by type", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "QoE"})
      insert_insight(org, ap, %{type: "capacity_saturation", urgency: "info", title: "Capacity"})

      insights = Insights.list_insights(org.id, type: "qoe_degradation")
      assert length(insights) == 1
      assert hd(insights).type == "qoe_degradation"
    end

    test "filters by urgency", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "critical", title: "Crit"})
      insert_insight(org, ap, %{type: "capacity_saturation", urgency: "info", title: "Info"})

      insights = Insights.list_insights(org.id, urgency: "critical")
      assert length(insights) == 1
      assert hd(insights).urgency == "critical"
    end

    test "respects limit", %{org: org, access_point: ap} do
      for i <- 1..5 do
        insert_insight(org, ap, %{
          type: "qoe_degradation",
          urgency: "warning",
          title: "Insight #{i}"
        })
      end

      insights = Insights.list_insights(org.id, limit: 3)
      assert length(insights) == 3
    end
  end

  describe "list_device_insights/1" do
    test "returns active insights for a specific device", %{org: org, device: device, access_point: ap} do
      insert_insight(org, ap, %{
        type: "qoe_degradation",
        urgency: "warning",
        title: "Device insight",
        device_id: device.id
      })

      # Create another device with its own insight
      other_device = device_fixture(%{organization_id: org.id})

      {:ok, other_ap} =
        %AccessPoint{}
        |> AccessPoint.changeset(%{
          organization_id: org.id,
          preseem_id: "ap-other-#{System.unique_integer([:positive])}",
          name: "Other AP",
          device_id: other_device.id,
          match_confidence: "auto_ip"
        })
        |> Repo.insert()

      insert_insight(org, other_ap, %{
        type: "capacity_saturation",
        urgency: "info",
        title: "Other device insight",
        device_id: other_device.id
      })

      insights = Insights.list_device_insights(device.id)
      assert length(insights) == 1
      assert hd(insights).title == "Device insight"
    end

    test "excludes dismissed insights", %{org: org, device: device, access_point: ap} do
      insert_insight(org, ap, %{
        type: "qoe_degradation",
        urgency: "warning",
        title: "Active",
        device_id: device.id
      })

      insert_insight(org, ap, %{
        type: "capacity_saturation",
        urgency: "info",
        title: "Dismissed",
        device_id: device.id,
        status: "dismissed"
      })

      insights = Insights.list_device_insights(device.id)
      assert length(insights) == 1
      assert hd(insights).title == "Active"
    end

    test "orders by urgency then date", %{org: org, device: device, access_point: ap} do
      insert_insight(org, ap, %{type: "capacity_saturation", urgency: "info", title: "Info", device_id: device.id})
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "critical", title: "Critical", device_id: device.id})

      insights = Insights.list_device_insights(device.id)
      assert Enum.at(insights, 0).urgency == "critical"
      assert Enum.at(insights, 1).urgency == "info"
    end
  end

  describe "dismiss_insight/1" do
    test "marks insight as dismissed with timestamp", %{org: org, access_point: ap} do
      {:ok, insight} = insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "Dismiss me"})

      assert {:ok, dismissed} = Insights.dismiss_insight(insight.id)
      assert dismissed.status == "dismissed"
      assert dismissed.dismissed_at
    end

    test "returns error for non-existent insight" do
      assert {:error, :not_found} = Insights.dismiss_insight(Ecto.UUID.generate())
    end
  end

  describe "dismiss_insights/1" do
    test "bulk dismisses multiple insights", %{org: org, access_point: ap} do
      {:ok, i1} = insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "One"})
      {:ok, i2} = insert_insight(org, ap, %{type: "capacity_saturation", urgency: "info", title: "Two"})
      {:ok, _i3} = insert_insight(org, ap, %{type: "model_underperforming", urgency: "info", title: "Three"})

      assert {:ok, 2} = Insights.dismiss_insights([i1.id, i2.id])

      # Verify i1 and i2 are dismissed
      assert Repo.get!(Insight, i1.id).status == "dismissed"
      assert Repo.get!(Insight, i2.id).status == "dismissed"

      # i3 remains active
      remaining = Insights.list_insights(org.id)
      assert length(remaining) == 1
      assert hd(remaining).title == "Three"
    end

    test "skips already dismissed insights", %{org: org, access_point: ap} do
      {:ok, i1} =
        insert_insight(org, ap, %{
          type: "qoe_degradation",
          urgency: "warning",
          title: "Already dismissed",
          status: "dismissed"
        })

      assert {:ok, 0} = Insights.dismiss_insights([i1.id])
    end
  end

  describe "generate_insights/1" do
    test "creates QoE degradation insight when score drops below 2 stddev", %{org: org, access_point: ap} do
      # AP has qoe_score 85.0 from setup. Create baseline with mean 90 and stddev 2.
      # 85 < 90 - 2*2 = 86, so should trigger warning.
      insert_baseline(ap, "qoe_score", "all", %{mean: 90.0, stddev: 2.0, p95: 95.0})

      assert {:ok, count} = Insights.generate_insights(org.id)
      assert count >= 1

      insights = Insights.list_insights(org.id, type: "qoe_degradation")
      assert length(insights) == 1

      insight = hd(insights)
      assert insight.type == "qoe_degradation"
      assert insight.urgency == "warning"
      assert insight.channel == "proactive"
      assert insight.preseem_access_point_id == ap.id
      assert insight.device_id == ap.device_id
    end

    test "creates critical QoE insight when score drops below 3 stddev", %{org: org} do
      device = device_fixture(%{organization_id: org.id})

      {:ok, ap} =
        %AccessPoint{}
        |> AccessPoint.changeset(%{
          organization_id: org.id,
          preseem_id: "ap-critical-#{System.unique_integer([:positive])}",
          name: "Critical AP",
          device_id: device.id,
          match_confidence: "auto_ip",
          model: "Ubiquiti AF5XHD",
          qoe_score: 70.0,
          subscriber_count: 20
        })
        |> Repo.insert()

      # qoe_score 70 < 90 - 3*5 = 75, so critical
      insert_baseline(ap, "qoe_score", "all", %{mean: 90.0, stddev: 5.0, p95: 98.0})

      assert {:ok, _count} = Insights.generate_insights(org.id)

      insights = Insights.list_insights(org.id, type: "qoe_degradation")
      assert length(insights) == 1
      assert hd(insights).urgency == "critical"
    end

    test "creates capacity saturation insight when subscribers exceed p95", %{org: org, access_point: ap} do
      # AP has subscriber_count 30 from setup. Create baseline with p95 of 25.
      insert_baseline(ap, "subscriber_count", "all", %{mean: 20.0, stddev: 3.0, p95: 25.0})

      assert {:ok, _count} = Insights.generate_insights(org.id)

      insights = Insights.list_insights(org.id, type: "capacity_saturation")
      assert length(insights) == 1

      insight = hd(insights)
      assert insight.urgency == "warning"
      assert insight.channel == "proactive"
    end

    test "creates model_underperforming insight when below fleet average", %{org: org} do
      # AP has qoe_score 85.0 and model "Ubiquiti AF5XHD".
      # Fleet avg is 120, so 85 < 120 * 0.8 = 96 -> should trigger.
      insert_fleet_profile(org, "Ubiquiti AF5XHD", %{avg_qoe_score: 120.0})

      assert {:ok, _count} = Insights.generate_insights(org.id)

      insights = Insights.list_insights(org.id, type: "model_underperforming")
      assert length(insights) == 1

      insight = hd(insights)
      assert insight.urgency == "info"
      assert insight.channel == "passive"
    end

    test "does not duplicate active insights for same type and AP", %{org: org, access_point: ap} do
      insert_baseline(ap, "subscriber_count", "all", %{mean: 20.0, stddev: 3.0, p95: 25.0})

      assert {:ok, count1} = Insights.generate_insights(org.id)
      assert count1 >= 1

      # Running again should not create duplicates
      assert {:ok, 0} = Insights.generate_insights(org.id)

      insights = Insights.list_insights(org.id, type: "capacity_saturation")
      assert length(insights) == 1
    end

    test "handles APs with no baselines gracefully", %{org: org} do
      # The setup AP has no baselines yet, and there are no fleet profiles
      # so generate should succeed with 0 insights
      assert {:ok, 0} = Insights.generate_insights(org.id)
    end

    test "skips APs without device_id (unmatched)", %{org: org} do
      %AccessPoint{}
      |> AccessPoint.changeset(%{
        organization_id: org.id,
        preseem_id: "ap-unmatched-#{System.unique_integer([:positive])}",
        name: "Unmatched AP",
        qoe_score: 10.0,
        subscriber_count: 100
      })
      |> Repo.insert!()

      # Even with extreme values, unmatched APs should not generate insights
      assert {:ok, 0} = Insights.generate_insights(org.id)
    end

    test "does not create QoE insight when score is within normal range", %{org: org, access_point: ap} do
      # AP qoe_score is 85. Baseline mean 85, stddev 5. 85 >= 85 - 2*5 = 75 -> no insight
      insert_baseline(ap, "qoe_score", "all", %{mean: 85.0, stddev: 5.0, p95: 93.0})

      assert {:ok, 0} = Insights.generate_insights(org.id)

      insights = Insights.list_insights(org.id, type: "qoe_degradation")
      assert Enum.empty?(insights)
    end

    test "does not create capacity insight when subscribers within p95", %{org: org, access_point: ap} do
      # AP subscriber_count is 30. Baseline p95 is 40 -> no insight
      insert_baseline(ap, "subscriber_count", "all", %{mean: 25.0, stddev: 5.0, p95: 40.0})

      assert {:ok, 0} = Insights.generate_insights(org.id)

      insights = Insights.list_insights(org.id, type: "capacity_saturation")
      assert Enum.empty?(insights)
    end
  end

  describe "list_insights/2 source filter" do
    test "filters by source", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "Preseem insight"})

      insert_insight_with_source(org, %{
        type: "snmp_cpu_high",
        urgency: "warning",
        title: "SNMP insight",
        source: "snmp"
      })

      preseem_insights = Insights.list_insights(org.id, source: "preseem")
      assert length(preseem_insights) == 1
      assert hd(preseem_insights).title == "Preseem insight"

      snmp_insights = Insights.list_insights(org.id, source: "snmp")
      assert length(snmp_insights) == 1
      assert hd(snmp_insights).title == "SNMP insight"
    end

    test "returns all sources when no source filter", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "Preseem"})

      insert_insight_with_source(org, %{
        type: "agent_offline",
        urgency: "critical",
        title: "System",
        source: "system"
      })

      insights = Insights.list_insights(org.id)
      assert length(insights) == 2
    end
  end

  describe "list_site_insights/2" do
    test "returns insights for a specific site", %{org: org} do
      {:ok, site} = Towerops.Sites.create_site(%{name: "Test Site", organization_id: org.id})
      {:ok, other_site} = Towerops.Sites.create_site(%{name: "Other Site", organization_id: org.id})

      insert_insight_with_source(org, %{
        type: "snmp_cpu_high",
        urgency: "warning",
        title: "Site insight",
        source: "snmp",
        site_id: site.id
      })

      insert_insight_with_source(org, %{
        type: "snmp_memory_high",
        urgency: "warning",
        title: "Other site insight",
        source: "snmp",
        site_id: other_site.id
      })

      insights = Insights.list_site_insights(site.id)
      assert length(insights) == 1
      assert hd(insights).title == "Site insight"
    end

    test "respects opts like limit and source", %{org: org} do
      {:ok, site} = Towerops.Sites.create_site(%{name: "Test Site", organization_id: org.id})

      for i <- 1..5 do
        insert_insight_with_source(org, %{
          type: "snmp_cpu_high",
          urgency: "warning",
          title: "Insight #{i}",
          source: "snmp",
          site_id: site.id
        })
      end

      insights = Insights.list_site_insights(site.id, limit: 3)
      assert length(insights) == 3
    end
  end

  describe "count_active_by_urgency/1" do
    test "returns counts grouped by urgency", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "critical", title: "Crit 1"})
      insert_insight(org, ap, %{type: "capacity_saturation", urgency: "critical", title: "Crit 2"})
      insert_insight(org, ap, %{type: "model_underperforming", urgency: "warning", title: "Warn 1"})

      insert_insight_with_source(org, %{type: "snmp_cpu_high", urgency: "info", title: "Info 1", source: "snmp"})

      counts = Insights.count_active_by_urgency(org.id)
      assert counts.critical == 2
      assert counts.warning == 1
      assert counts.info == 1
    end

    test "returns zeros when no insights", %{org: _org} do
      user = user_fixture()
      empty_org = organization_fixture(user.id)

      counts = Insights.count_active_by_urgency(empty_org.id)
      assert counts.critical == 0
      assert counts.warning == 0
      assert counts.info == 0
    end

    test "excludes dismissed insights", %{org: org, access_point: ap} do
      insert_insight(org, ap, %{type: "qoe_degradation", urgency: "critical", title: "Active"})

      insert_insight(org, ap, %{type: "capacity_saturation", urgency: "critical", title: "Dismissed", status: "dismissed"})

      counts = Insights.count_active_by_urgency(org.id)
      assert counts.critical == 1
    end
  end

  describe "insert_insight_if_new/2 generalized dedup" do
    test "deduplicates by device_id + type", %{org: org, device: device} do
      attrs = %{
        organization_id: org.id,
        device_id: device.id,
        type: "snmp_cpu_high",
        urgency: "warning",
        channel: "proactive",
        title: "CPU high",
        source: "snmp"
      }

      assert {:ok, _insight} = Insights.insert_insight_if_new(attrs)
      assert {:ok, :duplicate} = Insights.insert_insight_if_new(attrs)

      insights = Insights.list_insights(org.id, type: "snmp_cpu_high")
      assert length(insights) == 1
    end

    test "deduplicates by site_id + type", %{org: org} do
      {:ok, site} = Towerops.Sites.create_site(%{name: "Dedup Site", organization_id: org.id})

      attrs = %{
        organization_id: org.id,
        site_id: site.id,
        type: "snmp_cpu_high",
        urgency: "warning",
        channel: "proactive",
        title: "CPU high at site",
        source: "snmp"
      }

      assert {:ok, _insight} = Insights.insert_insight_if_new(attrs)
      assert {:ok, :duplicate} = Insights.insert_insight_if_new(attrs)
    end

    test "deduplicates by agent_token_id + type", %{org: org} do
      {:ok, token, _raw_token} = Towerops.Agents.create_agent_token(org.id, "Test Agent")

      attrs = %{
        organization_id: org.id,
        agent_token_id: token.id,
        type: "agent_offline",
        urgency: "critical",
        channel: "proactive",
        title: "Agent offline",
        source: "system"
      }

      assert {:ok, _insight} = Insights.insert_insight_if_new(attrs)
      assert {:ok, :duplicate} = Insights.insert_insight_if_new(attrs)
    end

    test "allows same type for different devices", %{org: org, device: device} do
      other_device = device_fixture(%{organization_id: org.id})

      base_attrs = %{
        organization_id: org.id,
        type: "snmp_cpu_high",
        urgency: "warning",
        channel: "proactive",
        title: "CPU high",
        source: "snmp"
      }

      assert {:ok, _} = Insights.insert_insight_if_new(Map.put(base_attrs, :device_id, device.id))
      assert {:ok, _} = Insights.insert_insight_if_new(Map.put(base_attrs, :device_id, other_device.id))

      insights = Insights.list_insights(org.id, type: "snmp_cpu_high")
      assert length(insights) == 2
    end
  end

  # Helpers

  defp insert_insight(org, ap, attrs) do
    full_attrs =
      Map.merge(
        %{
          organization_id: org.id,
          preseem_access_point_id: ap.id,
          channel: "proactive",
          status: "active"
        },
        attrs
      )

    %Insight{}
    |> Insight.changeset(full_attrs)
    |> Repo.insert()
  end

  defp insert_baseline(ap, metric_name, period, attrs) do
    full_attrs =
      Map.merge(
        %{
          preseem_access_point_id: ap.id,
          metric_name: metric_name,
          period: period,
          sample_count: 336,
          computed_at: DateTime.truncate(DateTime.utc_now(), :second)
        },
        attrs
      )

    %DeviceBaseline{}
    |> DeviceBaseline.changeset(full_attrs)
    |> Repo.insert!()
  end

  defp insert_insight_with_source(org, attrs) do
    full_attrs =
      Map.merge(
        %{
          organization_id: org.id,
          channel: "proactive",
          status: "active"
        },
        attrs
      )

    %Insight{}
    |> Insight.changeset(full_attrs)
    |> Repo.insert()
  end

  defp insert_fleet_profile(org, model, attrs) do
    full_attrs =
      Map.merge(
        %{
          organization_id: org.id,
          manufacturer: model |> String.split(" ") |> hd(),
          model: model,
          device_count: 10,
          computed_at: DateTime.truncate(DateTime.utc_now(), :second)
        },
        attrs
      )

    %FleetProfile{}
    |> FleetProfile.changeset(full_attrs)
    |> Repo.insert!()
  end
end
