#!/usr/bin/env python3
"""
Tests for the ``TESTFLIGHT_ONLY`` decision of ``manage_marketing_version``.

The incident this pins (GoVolt, CI run 33743284641, 2026-09-03): App Store
version 1.6.3 sat WAITING_FOR_REVIEW while the project had moved to 1.6.4.
The classifier looked only at rows AT the target, saw none, decided CREATE,
and App Store Connect answered 409 ENTITY_ERROR.RELATIONSHIP.INVALID ("You
cannot create a new version of the App in the current state") -- exit 4 on
every push to main until a person noticed. A target that itself matched the
in-review row used to exit 2 asking a human to wait or bump.

Both are the same fact about the APP: while any version is under review (or
approved and awaiting the developer's release click) the whole App Store slot
is locked. The decision is now ``TESTFLIGHT_ONLY``: stamp the locked version
string, take the next build number, upload to TestFlight, skip App Store
metadata, and never touch the project file.
"""

from __future__ import annotations

import io
import json
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 _run_decide_for_version, _v  # noqa: E402


def _boom(*_args, **_kwargs):
    raise AssertionError("a seam ran on the TESTFLIGHT_ONLY path")


class TestFlightOnlyTests(unittest.TestCase):
    """Any locked App Store version -> TESTFLIGHT_ONLY, nothing else runs."""

    def _decide(self, target: str, versions: list[dict]):
        """Run ``decide_for_version`` with every floor/create seam armed to
        raise, so the test proves the lock is decided before any of them."""
        buf = io.StringIO()
        with mock.patch.object(mmv, "assert_target_meets_floor", _boom), \
                mock.patch.object(mmv, "decide_create", _boom), \
                mock.patch.object(mmv, "create_or_reuse_with_stale", _boom), \
                mock.patch.object(mmv, "get_combined_floor", _boom), \
                mock.patch.object(mmv, "get_ground_truth_floor", _boom), \
                mock.patch.object(mmv, "maybe_auto_bump", _boom), \
                redirect_stderr(buf):
            result = mmv.decide_for_version(
                target, versions, app_id="111", token="tok",
            )
        return result, buf.getvalue()

    def test_sibling_in_review_is_testflight_only_under_the_sibling(self):
        """The incident shape: project at 1.6.4, 1.6.3 WAITING_FOR_REVIEW.
        The build is stamped 1.6.3 -- the locked version -- not 1.6.4."""
        versions = [
            _v("1.6.2", "READY_FOR_SALE", vid="rfs-162"),
            _v("1.6.3", "WAITING_FOR_REVIEW", vid="wfr-163"),
        ]
        result, stderr = self._decide("1.6.4", versions)
        self.assertEqual(result, {
            "decision": "TESTFLIGHT_ONLY",
            "versionString": "1.6.3",
            "appStoreVersionId": "",
            "state": "WAITING_FOR_REVIEW",
            "storeVersionLocked": "1.6.3",
        })
        self.assertIn("decision=TESTFLIGHT_ONLY versionString=1.6.3", stderr)
        self.assertIn("wfr-163", stderr)
        self.assertIn("::notice::", stderr)

    def test_target_equal_to_the_in_review_row_is_testflight_only(self):
        result, _ = self._decide("1.6.3", [_v("1.6.3", "IN_REVIEW", vid="ir")])
        self.assertEqual(result["decision"], "TESTFLIGHT_ONLY")
        self.assertEqual(result["versionString"], "1.6.3")
        self.assertEqual(result["state"], "IN_REVIEW")

    def test_pending_developer_release_locks_the_slot_too(self):
        """Apple approved 1.6.3 and the developer has not released it: a
        new App Store version cannot be created either."""
        result, _ = self._decide(
            "1.6.4", [_v("1.6.3", "PENDING_DEVELOPER_RELEASE", vid="pdr")])
        self.assertEqual(result["decision"], "TESTFLIGHT_ONLY")
        self.assertEqual(result["versionString"], "1.6.3")
        self.assertEqual(result["state"], "PENDING_DEVELOPER_RELEASE")

    def test_several_locked_rows_pick_the_highest_semver(self):
        versions = [
            _v("1.6.3", "PENDING_DEVELOPER_RELEASE", vid="pdr-163"),
            _v("1.6.10", "WAITING_FOR_REVIEW", vid="wfr-1610"),
            _v("1.6.9", "IN_REVIEW", vid="ir-169"),
        ]
        result, _ = self._decide("1.7.0", versions)
        self.assertEqual(result["versionString"], "1.6.10")
        self.assertEqual(result["storeVersionLocked"], "1.6.10")

    def test_nothing_in_review_leaves_the_ordinary_path_untouched(self):
        """Editable / terminal / archived rows do not lock the slot: the
        ordinary floor + classify path runs and CREATEs as before."""
        versions = [
            _v("1.6.2", "READY_FOR_SALE", vid="rfs-162"),
            _v("1.6.0", "REPLACED_WITH_NEW_VERSION", vid="old"),
            _v("1.6.1", "DEVELOPER_REJECTED", vid="dr"),
        ]
        result, stderr, calls = _run_decide_for_version(
            "1.6.4", versions=versions, ground_truth="1.6.2",
        )
        self.assertEqual(result["decision"], "CREATE")
        self.assertEqual(result["versionString"], "1.6.4")
        self.assertNotIn("storeVersionLocked", result)
        self.assertEqual(len(calls), 1)
        self.assertNotIn("TESTFLIGHT_ONLY", stderr)
        self.assertIsNone(mmv.store_version_locked(versions))

    def test_main_prints_the_locked_decision_as_json(self):
        """End to end through ``main()``: the JSON action.yml parses."""
        out_buf, err_buf = io.StringIO(), io.StringIO()
        env = {
            "ASC_KEY_ID": "kid", "ASC_ISSUER_ID": "iid",
            "ASC_KEY_PATH": "/tmp/ignored.p8", "APP_STORE_APPLE_ID": "111",
            "MARKETING_VERSION": "1.6.4",
        }
        rows = [_v("1.6.3", "WAITING_FOR_REVIEW", vid="wfr-163")]
        with mock.patch.dict(os.environ, env, clear=False), \
                mock.patch.object(mmv, "make_jwt", return_value="tok"), \
                mock.patch.object(mmv, "fetch_versions", return_value=rows), \
                mock.patch.object(mmv, "assert_target_meets_floor", _boom), \
                mock.patch("sys.stdout", out_buf), \
                redirect_stderr(err_buf):
            mmv.main()
        decision = json.loads(out_buf.getvalue().strip())
        self.assertEqual(decision["decision"], "TESTFLIGHT_ONLY")
        self.assertEqual(decision["versionString"], "1.6.3")
        self.assertEqual(decision["appStoreVersionId"], "")
        self.assertEqual(decision["storeVersionLocked"], "1.6.3")


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