#!/usr/bin/env python3
"""
Tests for mmv_decide_create.py — the orchestrator decisions made via
``_stale_editable_id`` and ``decide_create``.

Covers:
  - StaleEditableSelectionTests: ``_stale_editable_id`` decides the
    kwarg passed into ``create_or_reuse_with_stale``; lower-version
    editables get PATCH-renamed; equal/higher-version editables fail
    fast (Issue 10) so a future draft cannot be silently downgraded.
  - In-review rows (Issue 13) are skipped as PATCH-rename candidates;
    the CREATE path falls through to a plain POST.

The asc_version_create.reuse_stale_editable seam (PATCH /
DELETE+POST fallback / exit-5 path / logging) lives in
``test_reuse_stale_editable.py``. The 409 classifier path (POST
returns 409, never bumps, fails fast) lives in
``test_mmv_409_classifier.py``.
"""

from __future__ import annotations

import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from tests_common import (  # noqa: E402
    _assert_decide_exits,
    _run_decide_for_version,
    _v,
)


class StaleEditableSelectionTests(unittest.TestCase):
    """``_stale_editable_id`` decides the kwarg that ``decide_create``
    passes into ``create_or_reuse_with_stale``. Lower-version editables
    get PATCH-renamed to target; equal/higher-version editables fail
    fast (Issue 10) so a future draft cannot be silently downgraded."""

    def test_create_patch_renames_stale_editable_to_target(self):
        """target=1.1.0 with editable PFS at 1.0.5 -> CREATE via PATCH-rename;
        stale_editable_id kwarg must be the lower editable's id."""
        editable = _v("1.0.5", "PREPARE_FOR_SUBMISSION", vid="ed-105")
        result, stderr, calls = _run_decide_for_version(
            "1.1.0", versions=[editable], ground_truth="1.0.5",
        )
        self.assertEqual(result["decision"], "CREATE")
        self.assertEqual(result["versionString"], "1.1.0")
        self.assertEqual(len(calls), 1)
        self.assertEqual(calls[0].kwargs.get("stale_editable_id"), "ed-105")
        self.assertIn("PATCH-rename of stale editable id=ed-105", stderr)

    def test_create_no_stale_editable_no_patch(self):
        """target=1.1.0 with no editable at all -> CREATE via plain POST;
        stale_editable_id kwarg must be None."""
        result, stderr, calls = _run_decide_for_version(
            "1.1.0", versions=[], ground_truth=None,
        )
        self.assertEqual(result["decision"], "CREATE")
        self.assertEqual(len(calls), 1)
        self.assertIsNone(calls[0].kwargs.get("stale_editable_id"))
        self.assertNotIn("PATCH-rename", stderr)

    def test_create_blocked_by_higher_editable_pfs(self):
        """Issue 10: a future draft at 1.2.0 (PFS) must NOT be PATCH-renamed
        downward to 1.1.0; SystemExit(2) with downgrade wording."""
        higher = _v("1.2.0", "PREPARE_FOR_SUBMISSION", vid="ed-120")
        stderr = _assert_decide_exits(
            self, "1.1.0", [higher], ground_truth=None,
        )
        self.assertIn("1.2.0", stderr)
        self.assertIn("ed-120", stderr)
        self.assertIn("downgrade", stderr.lower())
        self.assertIn("PREPARE_FOR_SUBMISSION", stderr)

    def test_create_blocked_by_higher_editable_other_state(self):
        """Issue 10 covers every editable state, not just PFS. A draft
        in METADATA_REJECTED above target must also block CREATE. (A
        WAITING_FOR_REVIEW row locks the whole slot before this path is
        reached -- see test_mmv_testflight_only.py.)"""
        higher = _v("1.2.0", "METADATA_REJECTED", vid="ed-mr-120")
        stderr = _assert_decide_exits(
            self, "1.1.0", [higher], ground_truth=None,
        )
        self.assertIn("1.2.0", stderr)
        self.assertIn("ed-mr-120", stderr)
        self.assertIn("downgrade", stderr.lower())
        self.assertIn("METADATA_REJECTED", stderr)

    def test_create_blocked_by_equal_version_editable(self):
        """When an editable already sits at exactly target, REUSE wins
        via ``_find_match_at_target`` BEFORE decide_create is reached;
        no CREATE-path downgrade-rename is attempted."""
        match = _v("1.1.0", "PREPARE_FOR_SUBMISSION", vid="ed-110")
        result, stderr, calls = _run_decide_for_version(
            "1.1.0", versions=[match], ground_truth=None,
        )
        self.assertEqual(result["decision"], "REUSE")
        self.assertEqual(result["appStoreVersionId"], "ed-110")
        self.assertEqual(calls, [])
        self.assertIn("decision=REUSE", stderr)

    def test_lower_in_review_is_never_patch_renamed(self):
        """Issue 13: a WAITING_FOR_REVIEW row at version < target must
        NOT be PATCH-renamed (Apple is reviewing it). Two fences keep
        that true: the slot lock answers TESTFLIGHT_ONLY before the
        CREATE path is reached at all, and the CREATE path's own
        stale-editable pick still skips in-review rows as a backstop."""
        in_review = _v("1.0.5", "WAITING_FOR_REVIEW", vid="ed-wfr-105")
        result, stderr, calls = _run_decide_for_version(
            "1.1.0", versions=[in_review], ground_truth="1.0.5",
        )
        self.assertEqual(result["decision"], "TESTFLIGHT_ONLY")
        self.assertEqual(result["versionString"], "1.0.5")
        self.assertEqual(calls, [])
        self.assertNotIn("PATCH-rename", stderr)
        import mmv_decide_create
        self.assertIsNone(
            mmv_decide_create._stale_editable_id([in_review], "1.1.0"))


if __name__ == "__main__":
    unittest.main()


class EqualVersionWrittenDifferently(unittest.TestCase):
    """ASC auto-creates "1.0" with a new app record; Flutter ships "1.0.0".

    The skip above the guard is a STRING compare while the guard itself is a
    TUPLE compare, so this pair slipped past the skip and then tripped the
    guard — refusing the very FIRST deploy of every newly created app.
    "1.0" and "1.0.0" are the same version: renaming the editable normalises
    the string and downgrades nothing.
    """

    def test_equal_version_is_patch_renamed_not_refused(self):
        editable = _v("1.0", "PREPARE_FOR_SUBMISSION", vid="ed-10")
        result, stderr, calls = _run_decide_for_version(
            "1.0.0", versions=[editable], ground_truth=None,
        )
        self.assertEqual(result["decision"], "CREATE")
        self.assertEqual(result["versionString"], "1.0.0")
        self.assertEqual(calls[0].kwargs.get("stale_editable_id"), "ed-10")

    def test_a_genuinely_higher_draft_still_refuses(self):
        """The guard's real purpose must survive: renaming a draft AHEAD of
        the project would lose in-progress work."""
        editable = _v("1.2.0", "PREPARE_FOR_SUBMISSION", vid="ed-120")
        stderr = _assert_decide_exits(
            self, "1.1.0", versions=[editable], ground_truth=None,
        )
        self.assertIn("downgrade", stderr.lower())


if __name__ == "__main__":
    unittest.main()
