#!/usr/bin/env python3
"""
Shared test helpers for the manage_marketing_version test suite.

Imports the production module via `manage_marketing_version` so test
patches at the `mmv` namespace propagate through helper code paths
(``mmv.fetch_versions``, ``mmv.get_combined_floor``, etc. are seam
re-exports the helpers exercise late-bound).
"""

from __future__ import annotations

import io
import os
import sys
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


def _v(version: str, state: str, vid: str = "", created: str = "") -> dict:
    """Build a minimal versions-list entry matching fetch_versions() shape."""
    return {
        "versionString": version,
        "state": state,
        "id": vid or f"id-{version}-{state}",
        "createdDate": created,
    }


class _Env:
    """Scoped override of an env var so tests don't leak state."""

    def __init__(self, name: str, value: str | None) -> None:
        self._name = name
        self._value = value
        self._prev: str | None = None
        self._had: bool = False

    def __enter__(self) -> "_Env":
        self._had = self._name in os.environ
        self._prev = os.environ.get(self._name)
        if self._value is None:
            os.environ.pop(self._name, None)
        else:
            os.environ[self._name] = self._value
        return self

    def __exit__(self, *_exc) -> None:
        if self._had:
            os.environ[self._name] = self._prev or ""
        else:
            os.environ.pop(self._name, None)


_BUILD_SETTING_TOKENS = (
    "project.yml", "Info.plist", "xcconfig",
    "MARKETING_VERSION", "CFBundleShortVersionString",
)


def _assert_mentions_build_setting_source(test, stderr: str) -> None:
    """The cross-check error message MUST name at least one build-setting
    source so the user knows what to edit in their project."""
    test.assertTrue(
        any(tok in stderr for tok in _BUILD_SETTING_TOKENS),
        f"error must reference a build-setting source; got {stderr!r}",
    )


def _patch_floor(
    ground_truth: str | None,
    combined_floor: str | None = None,
    *,
    per_source: dict[str, str | None] | None = None,
):
    """Context-manager bundle for the two mmv-level floor seams. Tests use
    this to control what _assert_above_ground_truth sees without reaching
    into asc_version_fetch.

    `per_source` overrides the per-source breakdown surfaced in the
    error message; defaults to attributing the floor to appStoreVersions
    only so legacy tests stay green."""
    cf = combined_floor if combined_floor is not None else ground_truth
    breakdown = per_source if per_source is not None else {
        "appStoreVersions": cf,
        "preReleaseVersions": None,
        "buildsViaPreRelease": None,
    }
    return (
        mock.patch.object(mmv, "get_ground_truth_floor", return_value=ground_truth),
        mock.patch.object(mmv, "get_combined_floor", return_value=(cf, breakdown)),
    )


def _run_decide_for_version(
    target_version: str,
    versions: list[dict],
    *,
    new_id: str = "new-created-id",
    ground_truth: str | None = None,
    combined_floor: str | None = None,
    per_source: dict[str, str | None] | None = None,
):
    """
    Invoke decide_for_version() with create_or_reuse stubbed to return
    `new_id`. Returns (result, stderr_text, create_calls).

    `combined_floor` controls what mmv.get_combined_floor returns; if None,
    `ground_truth` doubles as the combined value too. `per_source` lets the
    test inject a real breakdown so message-content assertions can verify
    the binding-source name actually surfaces (the production path).
    """
    buf = io.StringIO()
    gt_patch, cf_patch = _patch_floor(
        ground_truth, combined_floor, per_source=per_source,
    )
    with mock.patch.object(
                mmv, "create_or_reuse_with_stale", return_value=new_id) as cor, \
            gt_patch, cf_patch, \
            redirect_stderr(buf):
        result = mmv.decide_for_version(
            target_version, versions, app_id="111", token="tok"
        )
    return result, buf.getvalue(), cor.call_args_list


def _assert_decide_exits(
    test, target: str, versions: list[dict],
    *, ground_truth: str | None, combined_floor: str | None = None,
    per_source: dict[str, str | None] | None = None,
    expected_code: int = 2,
) -> str:
    """Run decide_for_version expecting SystemExit. Returns the stderr text
    so the caller can make message-content assertions."""
    buf = io.StringIO()
    gt_patch, cf_patch = _patch_floor(
        ground_truth, combined_floor, per_source=per_source,
    )
    with gt_patch, cf_patch, redirect_stderr(buf):
        with test.assertRaises(SystemExit) as ctx:
            mmv.decide_for_version(target, versions, app_id="111", token="tok")
    test.assertEqual(ctx.exception.code, expected_code)
    return buf.getvalue()


class _FakeResponse:
    """Minimal stand-in for requests.Response."""

    def __init__(self, payload: dict, status_code: int = 200) -> None:
        self._payload = payload
        self.status_code = status_code
        self.text = ""

    def json(self) -> dict:
        return self._payload


def _mk_prerelease_entry(version: str, pid: str = "") -> dict:
    return {
        "id": pid or f"pr-{version}",
        "attributes": {"version": version, "platform": "IOS"},
    }
