#!/usr/bin/env python3
"""
Tests for ``manage_marketing_version``'s validation + main() entrypoint.

After round-8 split, this file holds only the input-validation and
``main()`` end-to-end scenarios plus the round-12 fail-closed
unknown-ASC-state rejection. Sibling test files cover the rest:

  * ``test_manage_marketing_version_match.py`` -- row-matching at the
    target versionString (REUSE / terminal-collision reject /
    in-review reject / DEVELOPER_REJECTED reuse).
  * ``test_manage_marketing_version_floor.py`` -- floor-vs-target
    compare scenarios.
  * ``test_manage_marketing_version_autoroll.py`` -- round-7 auto-roll
    lock-in (terminal collisions auto-roll; in-review rejects).

The original snowballing-bump bug guarded against: when the project
sat at MARKETING_VERSION=1.0.0 across CI runs, the old code computed
``next = combined_floor + patch`` -- the marketing version drifted
upward in TestFlight (1.0.0 -> 1.0.7 -> ...) without ever round-tripping
back to the repo. Each new run started from 1.0.0 and got bumped again,
server-side only.
"""

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 (  # noqa: E402
    _Env,
    _assert_decide_exits,
    _assert_mentions_build_setting_source,
    _v,
)


class ValidationAndMainTests(unittest.TestCase):
    """Semver validation, end-to-end main(), and missing-env scenarios."""

    def test_target_invalid_semver_exits_1(self):
        """Scenario 7: target 'foo' (invalid semver) -> SystemExit(1)."""
        stderr = _assert_decide_exits(
            self, "foo", [], ground_truth=None, expected_code=1,
        )
        self.assertIn("MARKETING_VERSION", stderr)
        _assert_mentions_build_setting_source(self, stderr)

    def test_main_end_to_end_with_marketing_version_env(self):
        """Scenario 8: xcodegen-style end-to-end via main()."""
        out_buf = io.StringIO()
        err_buf = io.StringIO()
        env_overrides = {
            "ASC_KEY_ID": "kid",
            "ASC_ISSUER_ID": "iid",
            "ASC_KEY_PATH": "/tmp/ignored.p8",
            "APP_STORE_APPLE_ID": "111",
            "MARKETING_VERSION": "1.0.0",
        }
        empty_breakdown = {"appStoreVersions": None,
                           "preReleaseVersions": None,
                           "buildsViaPreRelease": None}
        with mock.patch.dict(os.environ, env_overrides, clear=False), \
                mock.patch.object(mmv, "make_jwt", return_value="tok"), \
                mock.patch.object(mmv, "fetch_versions", return_value=[]), \
                mock.patch.object(mmv, "get_ground_truth_floor", return_value=None), \
                mock.patch.object(
                    mmv, "get_combined_floor",
                    return_value=(None, empty_breakdown)), \
                mock.patch.object(mmv, "create_or_reuse_with_stale",
                                  return_value="created-100"), \
                mock.patch("sys.stdout", out_buf), \
                redirect_stderr(err_buf):
            mmv.main()
        decision = json.loads(out_buf.getvalue().strip())
        self.assertEqual(decision["versionString"], "1.0.0")
        self.assertEqual(decision["decision"], "CREATE")
        self.assertEqual(decision["appStoreVersionId"], "created-100")

    def test_missing_marketing_version_env_exits_1(self):
        """Scenario 9: MARKETING_VERSION missing -> SystemExit(1)."""
        buf = io.StringIO()
        env_overrides = {
            "ASC_KEY_ID": "kid",
            "ASC_ISSUER_ID": "iid",
            "ASC_KEY_PATH": "/tmp/ignored.p8",
            "APP_STORE_APPLE_ID": "111",
        }
        with mock.patch.dict(os.environ, env_overrides, clear=False), \
                _Env("MARKETING_VERSION", None), \
                mock.patch.object(mmv, "make_jwt", return_value="tok"), \
                mock.patch.object(mmv, "fetch_versions", return_value=[]), \
                redirect_stderr(buf):
            with self.assertRaises(SystemExit) as ctx:
                mmv.main()
        self.assertEqual(ctx.exception.code, 1)
        stderr = buf.getvalue()
        self.assertIn("MARKETING_VERSION", stderr)

    def test_target_matches_unknown_state_exits_2_fail_closed(self):
        """Round 12: row exists at target with an ASC state in NONE of
        our allowlists (TERMINAL / IN_REVIEW / BLOCKING / REUSABLE) ->
        SystemExit(2) rather than silently CREATEing into a 409.

        The earlier classifier returned ``"none"`` for unknown states,
        so the script proceeded to CREATE -- which 409s against the
        existing row on every CI run. Failing closed surfaces the
        unrecognized state to the human so they can update the
        classifier or bump MARKETING_VERSION manually."""
        unknown = _v("1.0.0", "TOTALLY_NEW_APPLE_STATE", vid="ed-unk-100")
        stderr = _assert_decide_exits(
            self, "1.0.0", [unknown],
            ground_truth="1.0.0", combined_floor="1.0.0",
        )
        self.assertIn("1.0.0", stderr)
        self.assertIn("TOTALLY_NEW_APPLE_STATE", stderr)
        self.assertIn("unrecognized state", stderr)
        self.assertIn("ed-unk-100", stderr)
        self.assertIn("Update mmv classifier", stderr)
        _assert_mentions_build_setting_source(self, stderr)


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