#!/usr/bin/env python3
"""
Tests for ``asc_version_create.reuse_stale_editable`` — the
PATCH-rename / DELETE-then-POST fallback / failure-mode helper.

Split out of ``test_mmv_decide_create.py`` (Issue 15: file
function-count cap) so that:
  - ``test_reuse_stale_editable.py`` owns the asc_version_create
    seam: PATCH succeeds, PATCH fails -> DELETE+POST, PATCH+DELETE
    both fail -> exit 5, full-flow logging.
  - ``test_mmv_decide_create.py`` owns the orchestrator
    decisions made via ``_stale_editable_id`` and ``decide_create``.
"""

from __future__ import annotations

import io
import json
import sys
import unittest
from contextlib import redirect_stderr
from pathlib import Path

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

import asc_version_create  # noqa: E402


class _FakeResp:
    def __init__(self, status_code: int, *, payload: dict | None = None,
                 text: str = "", headers: dict | None = None) -> None:
        self.status_code = status_code
        self.text = text if text else (json.dumps(payload) if payload else "")
        self.headers = headers or {}
        self._payload = payload

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


class ReuseStaleEditablePatchTests(unittest.TestCase):
    """PATCH succeeds: stale editable is renamed to target."""

    def test_stale_editable_patch_path_reuses_and_skips_post(self):
        patch_calls: list[dict] = []
        post_calls: list[str] = []
        delete_calls: list[str] = []

        def fake_patch(app_id, existing_id, target, token):
            patch_calls.append({
                "app_id": app_id, "existing_id": existing_id,
                "target": target, "token": token,
            })
            return _FakeResp(200, payload={
                "data": {
                    "id": existing_id,
                    "type": "appStoreVersions",
                    "attributes": {"versionString": target},
                },
            })

        def fake_post(app_id, version, token):
            post_calls.append(version)
            return _FakeResp(201, payload={"data": {"id": "should-not-happen"}})

        def fake_delete(app_id, existing_id, token):
            delete_calls.append(existing_id)
            return _FakeResp(204)

        buf = io.StringIO()
        with redirect_stderr(buf):
            got = asc_version_create.reuse_stale_editable(
                app_id="111", existing_id="ed-101",
                target_version="1.0.6", token="tok",
                patch_fn=fake_patch, delete_fn=fake_delete,
                post_fn=fake_post,
            )

        self.assertEqual(len(patch_calls), 1)
        self.assertEqual(patch_calls[0]["existing_id"], "ed-101")
        self.assertEqual(patch_calls[0]["target"], "1.0.6")
        self.assertEqual(post_calls, [])
        self.assertEqual(delete_calls, [])
        self.assertEqual(got, "ed-101")


class ReuseStaleEditablePatchFailsDeleteCreateTests(unittest.TestCase):
    def test_stale_editable_patch_fails_falls_back_to_delete_create(self):
        sequence: list[str] = []

        def fake_patch(app_id, existing_id, target, token):
            sequence.append("PATCH")
            return _FakeResp(409, payload={
                "errors": [{"code": "SOME.REJECTION", "detail": "nope"}],
            })

        def fake_delete(app_id, existing_id, token):
            sequence.append(f"DELETE:{existing_id}")
            return _FakeResp(204)

        def fake_post(app_id, version, token):
            sequence.append(f"POST:{version}")
            return _FakeResp(201, payload={"data": {"id": "new-106"}})

        buf = io.StringIO()
        with redirect_stderr(buf):
            got = asc_version_create.reuse_stale_editable(
                app_id="111", existing_id="ed-101",
                target_version="1.0.6", token="tok",
                patch_fn=fake_patch, delete_fn=fake_delete,
                post_fn=fake_post,
            )

        self.assertEqual(sequence, ["PATCH", "DELETE:ed-101", "POST:1.0.6"])
        self.assertEqual(got, "new-106")


class ReuseStaleEditableDeleteFailsTests(unittest.TestCase):
    def test_stale_editable_delete_fails_exits_clean(self):
        def fake_patch(app_id, existing_id, target, token):
            return _FakeResp(409, payload={
                "errors": [{"code": "SOME.REJECTION", "detail": "nope"}],
            })

        def fake_delete(app_id, existing_id, token):
            return _FakeResp(409, payload={
                "errors": [{"code": "ENTITY_ERROR.STATE_INVALID",
                            "detail": "version locked by review"}],
            })

        def fake_post(app_id, version, token):
            raise AssertionError("POST must NOT be called when DELETE fails")

        buf = io.StringIO()
        with redirect_stderr(buf):
            with self.assertRaises(SystemExit) as ctx:
                asc_version_create.reuse_stale_editable(
                    app_id="111", existing_id="ed-101",
                    target_version="1.0.6", token="tok",
                    patch_fn=fake_patch, delete_fn=fake_delete,
                    post_fn=fake_post,
                )

        self.assertEqual(ctx.exception.code, 5)
        stderr = buf.getvalue()
        self.assertIn("ed-101", stderr)


class ReuseStaleEditableLoggingTests(unittest.TestCase):
    def test_reuse_stale_editable_logs_full_flow(self):
        def fake_patch(app_id, existing_id, target, token):
            return _FakeResp(409, payload={
                "errors": [{"code": "SOME.REJECTION", "detail": "nope"}],
            })

        def fake_delete(app_id, existing_id, token):
            return _FakeResp(204)

        def fake_post(app_id, version, token):
            return _FakeResp(201, payload={"data": {"id": "new-106"}})

        buf = io.StringIO()
        with redirect_stderr(buf):
            asc_version_create.reuse_stale_editable(
                app_id="111", existing_id="ed-101",
                target_version="1.0.6", token="tok",
                patch_fn=fake_patch, delete_fn=fake_delete,
                post_fn=fake_post,
            )

        stderr = buf.getvalue()
        self.assertIn("[reuse-stale]", stderr)
        self.assertIn("PATCH", stderr)
        self.assertIn("ed-101", stderr)
        self.assertIn("1.0.6", stderr)
        self.assertIn("PATCH failed", stderr)
        self.assertIn("DELETE", stderr)
        self.assertIn("retrying POST", stderr)


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