defmodule Microwaveprop.PartitionManagerPropertyTest do
  @moduledoc """
  Properties of the partition tiling produced by
  `Microwaveprop.PartitionManager.ensure_quarterly_partitions/2`. The
  goal is to pin down the math invariants (no gaps, no overlaps,
  exact 3-month bounds, deterministic naming) so refactors of the
  internal quarter helpers can't silently break them.

  All tests use a synthetic parent created inside the sandbox
  transaction so DDL rolls back cleanly when the test finishes.
  """
  use Microwaveprop.DataCase, async: true
  use ExUnitProperties

  alias Microwaveprop.PartitionManager
  alias Microwaveprop.Repo

  defp synthetic_parent do
    "test_partitioned_#{System.unique_integer([:positive])}"
  end

  defp create_partitioned_parent!(name) do
    Repo.query!("""
    CREATE TABLE public."#{name}" (
      valid_time timestamp without time zone NOT NULL,
      payload integer
    ) PARTITION BY RANGE (valid_time)
    """)
  end

  defp partitions_of(parent) do
    %Postgrex.Result{rows: rows} =
      Repo.query!(
        """
        SELECT child.relname, pg_get_expr(child.relpartbound, child.oid)
        FROM pg_inherits
        JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
        JOIN pg_class child  ON pg_inherits.inhrelid  = child.oid
        WHERE parent.relname = $1
        """,
        [parent]
      )

    rows
    |> Enum.map(fn [name, bound] ->
      [_, lo, hi] = Regex.run(~r/FROM \('([^']+)'\) TO \('([^']+)'\)/, bound)
      {name, NaiveDateTime.from_iso8601!(lo), NaiveDateTime.from_iso8601!(hi)}
    end)
    # Default `Enum.sort_by/2` uses `Kernel.<=/2` which falls back to
    # struct-field order on NaiveDateTime — NOT chronological. Pass
    # NaiveDateTime as the comparator module so sort_by uses
    # NaiveDateTime.compare/2.
    |> Enum.sort_by(fn {_, lo, _} -> lo end, NaiveDateTime)
  end

  property "partitions tile the lookahead range with no gaps and no overlaps" do
    check all(lookahead <- integer(0..8), max_runs: 25) do
      parent = synthetic_parent()
      create_partitioned_parent!(parent)

      _ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])

      partitions = partitions_of(parent)
      assert length(partitions) == lookahead + 1

      partitions
      |> Enum.chunk_every(2, 1, :discard)
      |> Enum.each(fn [{name_a, _, hi_a} = a, {name_b, lo_b, _} = b] ->
        # Adjacent partitions must touch exactly: end of A == start of B.
        assert NaiveDateTime.compare(hi_a, lo_b) == :eq,
               "gap/overlap: #{name_a} #{inspect(a)} → #{name_b} #{inspect(b)}"
      end)
    end
  end

  property "every partition spans exactly one quarter (89-92 days)" do
    check all(lookahead <- integer(0..6), max_runs: 15) do
      parent = synthetic_parent()
      create_partitioned_parent!(parent)

      _ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])

      Enum.each(partitions_of(parent), fn {_, lo, hi} ->
        days = NaiveDateTime.diff(hi, lo, :day)
        assert days in 89..92, "quarter spanned #{days} days"
        # Quarter starts on the 1st of Jan/Apr/Jul/Oct at midnight.
        assert lo.day == 1
        assert lo.hour == 0
        assert lo.minute == 0
        assert lo.second == 0
        assert lo.month in [1, 4, 7, 10]
      end)
    end
  end

  property "partition names follow <parent>_YYYY_MM and are unique" do
    check all(lookahead <- integer(0..6), max_runs: 15) do
      parent = synthetic_parent()
      create_partitioned_parent!(parent)

      _ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
      names = Enum.map(partitions_of(parent), fn {n, _, _} -> n end)

      Enum.each(names, fn n ->
        assert Regex.match?(~r/^#{parent}_\d{4}_(01|04|07|10)$/, n),
               "bad partition name: #{n}"
      end)

      assert length(names) == length(Enum.uniq(names)), "duplicate partition names"
    end
  end

  property "is idempotent — repeated calls are stable" do
    check all(
            lookahead <- integer(0..4),
            extra_runs <- integer(1..3),
            max_runs: 10
          ) do
      parent = synthetic_parent()
      create_partitioned_parent!(parent)

      _ = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
      first = partitions_of(parent)

      Enum.each(1..extra_runs, fn _ ->
        results = PartitionManager.ensure_quarterly_partitions(lookahead, [parent])
        # Every entry on a re-run must be :exists — never :created.
        assert Enum.all?(results, fn {_, _, status} -> status == :exists end)
      end)

      assert partitions_of(parent) == first
    end
  end

  property "respects multi-parent calls (independent tiles per parent)" do
    check all(
            lookahead <- integer(0..3),
            n_parents <- integer(1..3),
            max_runs: 8
          ) do
      parents = Enum.map(1..n_parents, fn _ -> synthetic_parent() end)
      Enum.each(parents, &create_partitioned_parent!/1)

      _ = PartitionManager.ensure_quarterly_partitions(lookahead, parents)

      Enum.each(parents, fn p ->
        assert length(partitions_of(p)) == lookahead + 1
      end)
    end
  end
end
