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

  alias Aprsme.PartitionManager

  @moduletag :migration

  setup do
    # Ensure counters row exists
    Repo.query!("""
    INSERT INTO packet_counters (counter_type, count, inserted_at, updated_at)
    VALUES ('total_packets', 0, NOW(), NOW())
    ON CONFLICT (counter_type) DO NOTHING
    """)

    :ok
  end

  # ── 1. Geometry index (idx_packets_location) on partitions ──────────────

  describe "geometry index (idx_packets_location)" do
    test "parent table has the GiST index" do
      {:ok, %{rows: rows}} =
        Repo.query("""
        SELECT indexname
        FROM pg_indexes
        WHERE tablename = 'packets'
          AND schemaname = 'public'
          AND indexname = 'idx_packets_location'
        """)

      assert length(rows) == 1
      assert [["idx_packets_location"]] = rows
    end

    test "index is valid (indisvalid = true) on parent" do
      {:ok, %{rows: rows}} =
        Repo.query("""
        SELECT indisvalid
        FROM pg_index
        WHERE indexrelid = 'idx_packets_location'::regclass
        """)

      assert [[true]] = rows
    end

    test "parent index has child partition indexes attached via pg_inherits" do
      # The parent GiST index should have child entries in pg_inherits
      # linking it to per-partition index relations.
      {:ok, %{rows: inherited}} =
        Repo.query("""
        SELECT inhrelid::regclass::text
        FROM pg_inherits
        WHERE inhparent = 'idx_packets_location'::regclass
        """)

      # At least one child partition index must be attached
      assert inherited != []

      # Each inherited index name ends with _location_idx (Postgres auto-named)
      # or _location_gist_idx (from migration build_and_attach).
      child_names = List.flatten(inherited)

      assert Enum.any?(child_names, fn name ->
               String.ends_with?(name, "_location_idx") or String.ends_with?(name, "_location_gist_idx")
             end)
    end

    test "idx_packets_location is a GiST index on location column" do
      # Verify the index uses GiST access method on the location column
      {:ok, %{rows: [[am_name]]}} =
        Repo.query("""
        SELECT am.amname
        FROM pg_index i
        JOIN pg_class c ON c.oid = i.indexrelid
        JOIN pg_am am ON am.oid = c.relam
        WHERE i.indexrelid = 'idx_packets_location'::regclass
        """)

      assert am_name == "gist"
    end
  end

  # ── 2. Counter reconciliation ───────────────────────────────────────────

  describe "packet counter reconciliation" do
    test "reconciliation sets counter to exact row count after INSERT" do
      now = DateTime.truncate(DateTime.utc_now(), :second)

      # Insert packets via raw SQL (gen_random_uuid() for binary UUID)
      for i <- 1..5 do
        Repo.query!(
          """
          INSERT INTO packets (id, sender, base_callsign, ssid, destination, raw_packet,
            data_type, lat, lon, received_at, inserted_at, updated_at)
          VALUES (gen_random_uuid(), 'RECON' || $1, 'RECON', $1::text, 'APRS',
            'RECON' || $1 || '>APRS:test', 'position', 39.0, -98.0,
            $2, $2, $2)
          ON CONFLICT DO NOTHING
          """,
          ["#{i}", DateTime.add(now, -i * 60, :second)]
        )
      end

      {:ok, %{rows: [[total]]}} = Repo.query("SELECT COUNT(*) FROM packets")
      assert total >= 5

      # Set counter to wrong value (simulate drift)
      Repo.query!("""
      UPDATE packet_counters
      SET count = 999, updated_at = NOW()
      WHERE counter_type = 'total_packets'
      """)

      # Run reconciliation (from migration 20260726000002)
      Repo.query!("""
      UPDATE packet_counters
      SET count = (SELECT COUNT(*) FROM packets),
          updated_at = NOW()
      WHERE counter_type = 'total_packets'
      """)

      {:ok, %{rows: [[counter_val]]}} =
        Repo.query("SELECT count FROM packet_counters WHERE counter_type = 'total_packets'")

      {:ok, %{rows: [[actual_count]]}} =
        Repo.query("SELECT COUNT(*) FROM packets")

      assert counter_val == actual_count
    end

    test "reconciliation resets to zero on empty packets table" do
      # Set counter to wrong value
      Repo.query!("""
      UPDATE packet_counters
      SET count = 123, updated_at = NOW()
      WHERE counter_type = 'total_packets'
      """)

      # Run reconciliation
      Repo.query!("""
      UPDATE packet_counters
      SET count = (SELECT COUNT(*) FROM packets),
          updated_at = NOW()
      WHERE counter_type = 'total_packets'
      """)

      {:ok, %{rows: [[counter_val]]}} =
        Repo.query("SELECT count FROM packet_counters WHERE counter_type = 'total_packets'")

      {:ok, %{rows: [[real_count]]}} =
        Repo.query("SELECT COUNT(*) FROM packets")

      assert counter_val == real_count
    end

    test "get_packet_count() function returns the reconciled counter value" do
      Repo.query!("""
      UPDATE packet_counters
      SET count = (SELECT COUNT(*) FROM packets),
          updated_at = NOW()
      WHERE counter_type = 'total_packets'
      """)

      {:ok, %{rows: [[func_count]]}} =
        Repo.query("SELECT get_packet_count()")

      {:ok, %{rows: [[real_count]]}} =
        Repo.query("SELECT COUNT(*) FROM packets")

      assert func_count == real_count
    end

    test "triggers keep counter in sync on insert and delete" do
      Repo.query!("""
      UPDATE packet_counters
      SET count = (SELECT COUNT(*) FROM packets),
          updated_at = NOW()
      WHERE counter_type = 'total_packets'
      """)

      {:ok, %{rows: [[before]]}} =
        Repo.query("SELECT count FROM packet_counters WHERE counter_type = 'total_packets'")

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

      # Insert via raw SQL — triggers should fire
      {:ok, %{rows: [[inserted_id]]}} =
        Repo.query(
          """
          INSERT INTO packets (id, sender, base_callsign, ssid, destination, raw_packet,
            data_type, lat, lon, location, has_position, received_at, inserted_at, updated_at)
          VALUES (gen_random_uuid(), 'TRIGTST', 'TRIGTST', '0', 'APRS', 'TRIGTST>APRS:test',
            'position', 39.0, -98.0, ST_SetSRID(ST_MakePoint(-98.0, 39.0), 4326), true,
            $1, $1, $1)
          RETURNING id
          """,
          [now]
        )

      {:ok, %{rows: [[after_insert]]}} =
        Repo.query("SELECT count FROM packet_counters WHERE counter_type = 'total_packets'")

      assert after_insert == before + 1

      # Delete the packet — trigger should decrement
      Repo.query!("DELETE FROM packets WHERE id = $1", [inserted_id])

      {:ok, %{rows: [[after_delete]]}} =
        Repo.query("SELECT count FROM packet_counters WHERE counter_type = 'total_packets'")

      assert after_delete == before
    end
  end

  # ── 3. Partition boundary cutoff ────────────────────────────────────────

  describe "partition boundary cutoff" do
    setup do
      # Ensure today's partition exists so raw INSERTs route correctly
      {:ok, _} = PartitionManager.ensure_partitions_exist()
      :ok
    end

    test "FOR VALUES FROM ... TO ... uses strict < upper bound" do
      date = Date.utc_today()
      partition_name = PartitionManager.partition_name(date)
      {from_dt, to_dt} = PartitionManager.partition_range(date)

      # Insert packet at exact partition start (>= FROM bound)
      {:ok, %{rows: [[start_id]]}} =
        Repo.query(
          """
          INSERT INTO packets (id, sender, base_callsign, ssid, destination, raw_packet,
            data_type, received_at, inserted_at, updated_at)
          VALUES (gen_random_uuid(), 'BDSTART', 'BDSTART', '0', 'APRS', 'BDSTART>APRS:test',
            'status', $1, $1, $1)
          RETURNING id
          """,
          [from_dt]
        )

      # Insert packet at exact partition end (< TO bound)
      {:ok, %{rows: [[end_id]]}} =
        Repo.query(
          """
          INSERT INTO packets (id, sender, base_callsign, ssid, destination, raw_packet,
            data_type, received_at, inserted_at, updated_at)
          VALUES (gen_random_uuid(), 'BDEND', 'BDEND', '0', 'APRS', 'BDEND>APRS:test',
            'status', $1, $1, $1)
          RETURNING id
          """,
          [to_dt]
        )

      # Packet at FROM should be in this partition (>= FROM)
      {:ok, %{rows: [[start_count]]}} =
        Repo.query("SELECT COUNT(*) FROM #{partition_name} WHERE id = $1::uuid", [start_id])

      assert start_count == 1

      # Packet at TO should NOT be in this partition (< TO, strict upper bound)
      {:ok, %{rows: [[end_count]]}} =
        Repo.query("SELECT COUNT(*) FROM #{partition_name} WHERE id = $1::uuid", [end_id])

      assert end_count == 0
    end

    test "packet at boundary timestamp is findable via parent table" do
      date = Date.utc_today()
      {_from_dt, to_dt} = PartitionManager.partition_range(date)

      {:ok, %{rows: [[end_id]]}} =
        Repo.query(
          """
          INSERT INTO packets (id, sender, base_callsign, ssid, destination, raw_packet,
            data_type, received_at, inserted_at, updated_at)
          VALUES (gen_random_uuid(), 'ROUTABLE', 'ROUTABLE', '0', 'APRS', 'ROUTABLE>APRS:test',
            'status', $1, $1, $1)
          RETURNING id
          """,
          [to_dt]
        )

      # Packet should be findable via parent table query
      {:ok, %{rows: [[_]]}} =
        Repo.query("SELECT sender FROM packets WHERE id = $1::uuid", [end_id])

      # Verify it's NOT in today's partition (boundary exclusive)
      today_name = PartitionManager.partition_name(date)

      {:ok, %{rows: [[in_today]]}} =
        Repo.query("SELECT COUNT(*) FROM #{today_name} WHERE id = $1::uuid", [end_id])

      assert in_today == 0
    end

    test "drop_old_partitions drops partitions whose upper bound <= cutoff" do
      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 old_name in PartitionManager.list_partitions()

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

      assert old_name in dropped
    end

    test "boundary-day partition is retained when its upper bound > cutoff" 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
  end
end
