#!/usr/bin/env python3
"""
Tests for the auto-roll-vs-in-review policy in
``manage_marketing_version.decide_for_version``.

Round-7 codex finding lock-in: auto-roll fires for TERMINAL collisions
(READY_FOR_SALE etc.) but NEVER for in-review collisions
(WAITING_FOR_REVIEW / IN_REVIEW / PENDING_DEVELOPER_RELEASE). Apple is
reviewing (or holding) the row, and advancing past it would race the
review process -- so those rows lock the App Store slot and the decision
is TESTFLIGHT_ONLY, whatever the auto-bump policy: the build ships to
TestFlight under the locked version and no project file is written.

Extracted from ``test_manage_marketing_version.py`` so each test file
stays under the 10-functions-per-file cap (round 8).
"""

from __future__ import annotations

import io
import os
import sys
import unittest
from contextlib import redirect_stderr
from pathlib import Path
from unittest import mock

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

import manage_marketing_version as mmv  # noqa: E402

from tests_common import _v  # noqa: E402


class AutoRollTerminalVsInReviewTests(unittest.TestCase):
    """auto-roll fires only for terminal collisions; in-review rejects."""

    def _run(self, asc_state, policy):
        """Drive ``decide_for_version`` with a single floor row at 1.0.5
        in ``asc_state`` and the given auto-bump ``policy``. Returns
        ``(result_or_none, exit_code_or_none, stderr)``."""
        floor = "1.0.5"
        per_source = {
            "appStoreVersions": floor,
            "preReleaseVersions": None,
            "buildsViaPreRelease": None,
        }
        with mock.patch.object(
                mmv, "create_or_reuse_with_stale", return_value="rolled-id"), \
                mock.patch.object(
                    mmv, "get_combined_floor",
                    return_value=(floor, per_source)), \
                mock.patch.object(
                    mmv, "get_ground_truth_floor", return_value=floor), \
                mock.patch(
                    "version_utils.write_marketing_version",
                    return_value=True), \
                mock.patch.dict(
                    os.environ,
                    {
                        "MARKETING_VERSION_AUTO_BUMP": policy,
                        # Round-9 persistence-context gate: auto-bump
                        # only fires on push to the default branch.
                        # Pin those envs here so the auto-roll tests
                        # exercise the actual auto-bump code path
                        # (rather than the persistence-context refusal).
                        "GITHUB_EVENT_NAME": "push",
                        "GITHUB_REF": "refs/heads/main",
                        "GITHUB_DEFAULT_BRANCH": "main",
                    },
                    clear=False):
            versions = [_v(floor, asc_state)]
            buf = io.StringIO()
            with redirect_stderr(buf):
                try:
                    result = mmv.decide_for_version(
                        floor, versions, app_id="111", token="tok",
                    )
                    return result, None, buf.getvalue()
                except SystemExit as exc:
                    return None, exc.code, buf.getvalue()

    def test_ready_for_sale_with_autobump_creates_next_patch(self):
        result, exit_code, _ = self._run("READY_FOR_SALE", "patch")
        self.assertIsNone(exit_code, "auto-roll must succeed for terminal")
        self.assertEqual(result["decision"], "CREATE")
        self.assertEqual(result["versionString"], "1.0.6")

    def test_pending_apple_release_with_autobump_creates_next_patch(self):
        result, exit_code, _ = self._run("PENDING_APPLE_RELEASE", "patch")
        self.assertIsNone(exit_code)
        self.assertEqual(result["decision"], "CREATE")
        self.assertEqual(result["versionString"], "1.0.6")

    def test_processing_for_app_store_with_autobump_creates_next_patch(self):
        result, exit_code, _ = self._run("PROCESSING_FOR_APP_STORE", "patch")
        self.assertIsNone(exit_code)
        self.assertEqual(result["decision"], "CREATE")
        self.assertEqual(result["versionString"], "1.0.6")

    def test_waiting_for_review_with_autobump_is_testflight_only(self):
        """Round-7: in-review must NEVER auto-roll regardless of policy.
        The slot is locked, so the build goes to TestFlight under the
        locked version and the project file is left alone."""
        result, exit_code, stderr = self._run("WAITING_FOR_REVIEW", "patch")
        self.assertIsNone(exit_code)
        self.assertEqual(result["decision"], "TESTFLIGHT_ONLY")
        self.assertEqual(result["versionString"], "1.0.5")
        self.assertEqual(result["appStoreVersionId"], "")
        self.assertIn("decision=TESTFLIGHT_ONLY", stderr)
        self.assertNotIn("auto-bumped", stderr)

    def test_in_review_with_autobump_is_testflight_only(self):
        result, exit_code, stderr = self._run("IN_REVIEW", "patch")
        self.assertIsNone(exit_code)
        self.assertEqual(result["decision"], "TESTFLIGHT_ONLY")
        self.assertEqual(result["state"], "IN_REVIEW")
        self.assertNotIn("auto-bumped", stderr)

    def test_pending_developer_release_with_autobump_is_testflight_only(self):
        """Round-11: PENDING_DEVELOPER_RELEASE means Apple approved
        the build and is waiting on the developer's manual release
        click. Auto-rolling past it would bypass the developer's
        release decision -- silently shipping the next patch while
        the approved build sits unreleased. The slot is locked the same
        way a review locks it: TestFlight only, under 1.0.5."""
        result, exit_code, stderr = self._run(
            "PENDING_DEVELOPER_RELEASE", "patch",
        )
        self.assertIsNone(exit_code)
        self.assertEqual(result["decision"], "TESTFLIGHT_ONLY")
        self.assertEqual(result["versionString"], "1.0.5")
        self.assertEqual(result["state"], "PENDING_DEVELOPER_RELEASE")
        self.assertIn("PENDING_DEVELOPER_RELEASE", stderr)

    def test_terminal_with_policy_none_rejects(self):
        result, exit_code, stderr = self._run("READY_FOR_SALE", "none")
        self.assertIsNone(result)
        self.assertEqual(exit_code, 2)
        self.assertIn("READY_FOR_SALE", stderr)

    def test_in_review_with_policy_none_is_testflight_only(self):
        """The lock does not depend on the auto-bump policy at all."""
        result, exit_code, stderr = self._run("WAITING_FOR_REVIEW", "none")
        self.assertIsNone(exit_code)
        self.assertEqual(result["decision"], "TESTFLIGHT_ONLY")
        self.assertIn("decision=TESTFLIGHT_ONLY", stderr)

    def test_editable_state_reuses_unchanged(self):
        """Sanity: PREPARE_FOR_SUBMISSION at target REUSEs unchanged."""
        result, exit_code, _ = self._run("PREPARE_FOR_SUBMISSION", "patch")
        self.assertIsNone(exit_code)
        self.assertEqual(result["decision"], "REUSE")
        self.assertEqual(result["versionString"], "1.0.5")


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