defmodule Aprsme.PartitionManagerTest do
  use Aprsme.DataCase, async: false

  alias Aprsme.Packet
  alias Aprsme.PartitionManager

  setup do
    # Defensive: any rows in packets_default block ATTACH PARTITION on
    # overlapping ranges with a check_violation. Wipe the parent table
    # so partition tests can freely create/drop partitions.
    Repo.delete_all(Packet)
    :ok
  end

  describe "partition_name/1" do
    test "generates correct name for a date" do
      assert PartitionManager.partition_name(~D[2026-02-20]) == "packets_20260220"
      assert PartitionManager.partition_name(~D[2026-01-01]) == "packets_20260101"
      assert PartitionManager.partition_name(~D[2025-12-31]) == "packets_20251231"
    end
  end

  describe "partition_range/1" do
    test "returns start and end timestamps for a date" do
      {start_dt, end_dt} = PartitionManager.partition_range(~D[2026-02-20])
      assert start_dt == ~U[2026-02-20 00:00:00Z]
      assert end_dt == ~U[2026-02-21 00:00:00Z]
    end

    test "handles month boundaries" do
      {_start_dt, end_dt} = PartitionManager.partition_range(~D[2026-01-31])
      assert end_dt == ~U[2026-02-01 00:00:00Z]
    end

    test "handles year boundaries" do
      {_start_dt, end_dt} = PartitionManager.partition_range(~D[2025-12-31])
      assert end_dt == ~U[2026-01-01 00:00:00Z]
    end
  end

  describe "ensure_partitions_exist/0" do
    test "creates partitions for today and next 2 days" do
      today = Date.utc_today()

      {:ok, created} = PartitionManager.ensure_partitions_exist()

      assert match?([_ | _], created)

      # Verify partitions exist in the database
      for offset <- 0..2 do
        date = Date.add(today, offset)
        name = PartitionManager.partition_name(date)

        assert {:ok, %{num_rows: 1}} =
                 Repo.query(
                   "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'",
                   [name]
                 )
      end
    end

    test "is idempotent - calling twice doesn't error" do
      {:ok, _created1} = PartitionManager.ensure_partitions_exist()
      {:ok, _created2} = PartitionManager.ensure_partitions_exist()
    end
  end

  describe "drop_old_partitions/1" do
    test "drops partitions older than retention days" do
      # Create a partition for 30 days ago
      old_date = Date.add(Date.utc_today(), -30)
      name = PartitionManager.partition_name(old_date)
      {from_dt, to_dt} = PartitionManager.partition_range(old_date)

      Repo.query!(
        "CREATE TABLE IF NOT EXISTS #{name} PARTITION OF packets FOR VALUES FROM ('#{DateTime.to_iso8601(from_dt)}') TO ('#{DateTime.to_iso8601(to_dt)}')"
      )

      # Verify it exists
      assert {:ok, %{num_rows: 1}} =
               Repo.query(
                 "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'",
                 [name]
               )

      # Drop partitions older than 7 days
      {:ok, dropped} = PartitionManager.drop_old_partitions(7)

      assert name in dropped

      # Verify it's gone
      assert {:ok, %{num_rows: 0}} =
               Repo.query(
                 "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'",
                 [name]
               )
    end

    test "does not drop recent partitions" do
      # Ensure today's partition exists
      {:ok, _} = PartitionManager.ensure_partitions_exist()

      today_name = PartitionManager.partition_name(Date.utc_today())

      {:ok, dropped} = PartitionManager.drop_old_partitions(7)

      refute today_name in dropped

      # Verify today's partition still exists
      assert {:ok, %{num_rows: 1}} =
               Repo.query(
                 "SELECT 1 FROM pg_tables WHERE tablename = $1 AND schemaname = 'public'",
                 [today_name]
               )
    end

    test "keeps a cutoff-day partition while it still contains retained packets" do
      cutoff_date = Date.add(Date.utc_today(), -7)
      name = PartitionManager.partition_name(cutoff_date)
      {from_dt, to_dt} = PartitionManager.partition_range(cutoff_date)

      Repo.query!(
        "CREATE TABLE IF NOT EXISTS #{name} PARTITION OF packets FOR VALUES FROM ('#{DateTime.to_iso8601(from_dt)}') TO ('#{DateTime.to_iso8601(to_dt)}')"
      )

      {:ok, dropped} = PartitionManager.drop_old_partitions(7)

      refute name in dropped
    end

    test "drops a partition only after its upper bound passes the rolling cutoff" do
      expired_date = Date.add(Date.utc_today(), -8)
      name = PartitionManager.partition_name(expired_date)
      {from_dt, to_dt} = PartitionManager.partition_range(expired_date)

      Repo.query!(
        "CREATE TABLE IF NOT EXISTS #{name} PARTITION OF packets FOR VALUES FROM ('#{DateTime.to_iso8601(from_dt)}') TO ('#{DateTime.to_iso8601(to_dt)}')"
      )

      {:ok, dropped} = PartitionManager.drop_old_partitions(7)

      assert name in dropped
    end
  end

  describe "list_partitions/0" do
    test "returns list of existing partition names" do
      {:ok, _} = PartitionManager.ensure_partitions_exist()

      partitions = PartitionManager.list_partitions()

      # credo:disable-for-next-line Credo.Check.Performance.Length
      assert length(partitions) >= 3

      today_name = PartitionManager.partition_name(Date.utc_today())
      assert today_name in partitions
    end
  end

  describe "init/1 and handle_info/2" do
    test "init/1 returns :ok state without scheduling in :test env" do
      original = Application.get_env(:aprsme, :env)
      Application.put_env(:aprsme, :env, :test)

      try do
        assert {:ok, %{}} = PartitionManager.init([])
        # The :manage_partitions message should NOT arrive since we're in :test.
        refute_receive :manage_partitions, 100
      after
        Application.put_env(:aprsme, :env, original)
      end
    end

    test "init/1 schedules :manage_partitions in non-test envs" do
      original = Application.get_env(:aprsme, :env)
      Application.put_env(:aprsme, :env, :prod)

      try do
        assert {:ok, %{}} = PartitionManager.init([])
        # send/2 is synchronous to self() — should be in the mailbox already.
        assert_receive :manage_partitions, 1000
      after
        Application.put_env(:aprsme, :env, original)
      end
    end

    test "handle_info(:manage_partitions) reschedules and returns :noreply" do
      # This exercises the create + drop path via real DB access.
      assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{})
      # Next tick is scheduled via Process.send_after at 1h, so we won't receive it.
    end
  end

  describe "validate_partition_name! (via create_partition path)" do
    test "ensure_partitions_exist rejects dates that would produce malformed names" do
      # partition_name is a pure date formatter — the format is guaranteed
      # valid for every Date. This test just documents that normal dates pass.
      for offset <- -7..7 do
        name = PartitionManager.partition_name(Date.add(Date.utc_today(), offset))
        assert name =~ ~r/^packets_\d{8}$/
      end
    end
  end

  describe "drop_old_partitions/1 default retention" do
    test "dropping with the default 7-day retention is a no-op when no partitions are old" do
      # Ensure today's partition exists.
      {:ok, _} = PartitionManager.ensure_partitions_exist()
      # Calling without args uses the default retention_days = 7.
      assert {:ok, dropped} = PartitionManager.drop_old_partitions()
      today_name = PartitionManager.partition_name(Date.utc_today())
      refute today_name in dropped
    end
  end

  describe "start_link/1" do
    test "starts the GenServer process with the module name" do
      # If a previous test left one running, just confirm start_link works.
      case Process.whereis(PartitionManager) do
        nil ->
          {:ok, pid} = PartitionManager.start_link([])
          assert Process.alive?(pid)
          GenServer.stop(pid)

        pid ->
          assert Process.alive?(pid)
      end
    end
  end

  describe "manage_partitions/0 idempotent paths" do
    test "running :manage_partitions twice covers the {:ok, []} branches" do
      # First run creates today/+1/+2 partitions (or finds them existing) and
      # drops nothing old. Second run finds everything in place — both
      # ensure_partitions_exist and drop_old_partitions return {:ok, []}.
      assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{})
      assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{})
    end
  end

  describe "manage_partitions/0 via :manage_partitions handler with non-trivial work" do
    test "logs created+dropped partitions via the success branches" do
      # Pre-create a partition far in the past so manage_partitions has
      # something to drop.
      old_date = Date.add(Date.utc_today(), -30)
      old_name = PartitionManager.partition_name(old_date)
      {from_dt, to_dt} = PartitionManager.partition_range(old_date)

      Repo.query!(
        "CREATE TABLE IF NOT EXISTS #{old_name} PARTITION OF packets FOR VALUES FROM ('#{DateTime.to_iso8601(from_dt)}') TO ('#{DateTime.to_iso8601(to_dt)}')"
      )

      assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{})

      refute old_name in PartitionManager.list_partitions()
    end
  end
end
