#!/usr/bin/env python3
"""Contract test for CARBON persona feedback and breadth/depth exploration."""

from __future__ import annotations

import json
import subprocess
import sys
import tempfile
from pathlib import Path


PLUGIN = Path(__file__).resolve().parents[1]
RUN = PLUGIN / "runtime" / "carbon" / "scripts" / "carbon_run.py"
EVIDENCE = PLUGIN / "runtime" / "carbon" / "scripts" / "carbon_evidence.py"


def invoke(script: Path, *args: str, ok: bool = True) -> subprocess.CompletedProcess[str]:
    result = subprocess.run([sys.executable, str(script), *args], text=True, capture_output=True, check=False)
    if ok and result.returncode:
        raise AssertionError(f"{script.name} {' '.join(args)} failed:\n{result.stderr}")
    if not ok and result.returncode == 0:
        raise AssertionError(f"{script.name} {' '.join(args)} unexpectedly succeeded")
    return result


with tempfile.TemporaryDirectory(prefix="carbon-perspectives-") as temporary:
    root = Path(temporary)
    empty_plan = root / "empty-personas.json"
    empty_plan.write_text(json.dumps({
        "title": "Invalid empty persona plan", "personas": [],
        "tests": [{"id": "CAR-EMPTY-001", "title": "Static check", "priority": "P1", "type": "static", "reason": "fixture", "provenance": ["fixture"], "prerequisites": [], "data": [], "steps": ["Inspect"], "oracle": "Result exists", "estimatedSeconds": 1, "dependencies": [], "parallelGroup": "static", "adapter": "code-inspection", "flags": {}}],
    }), encoding="utf-8")
    rejected_empty = invoke(RUN, "init", "--root", str(root), "--plan", str(empty_plan), "--mode", "quick", ok=False)
    if "requires at least one planned persona journey" not in rejected_empty.stderr:
        raise AssertionError("an explicitly empty persona plan bypassed the every-run contract")
    plan_path = root / "plan.json"
    plan_path.write_text(json.dumps({
        "title": "Persona and exploration pass",
        "target": "https://example.test/workspace",
        "testStrategy": {"requested": "balanced", "selected": "balanced", "rationale": "Map the workspace, then drill into save recovery."},
        "personas": [
            {"id": "PERSONA-PRIMARY", "name": "Primary user", "role": "workspace owner", "goal": "save a change"},
            {"id": "PERSONA-FIRST-TIME", "name": "First-time user", "role": "new collaborator", "goal": "understand and complete the flow"},
        ],
        "explorationCharters": [
            {"id": "CHARTER-BREADTH", "strategy": "breadth", "title": "Map the workspace", "mission": "Sample the page, controls, and transitions."},
            {"id": "CHARTER-DEPTH", "strategy": "depth", "title": "Drill into save recovery", "mission": "Exercise retry and recovery around save."},
        ],
        "tests": [{
            "id": "CAR-PER-001", "title": "Workspace save", "priority": "P1", "type": "browser",
            "reason": "Exercise the primary flow", "provenance": ["fixture"], "prerequisites": [],
            "data": ["synthetic workspace"], "steps": ["Open workspace", "Save change"],
            "oracle": "Save is visible and recoverable", "estimatedSeconds": 30, "dependencies": [],
            "parallelGroup": "browser", "adapter": "isolated-browser", "flags": {},
        }],
    }), encoding="utf-8")
    run_dir = Path(invoke(RUN, "init", "--root", str(root), "--plan", str(plan_path), "--mode", "medium").stdout.strip())
    invoke(RUN, "start", "--run-dir", str(run_dir))
    invoke(RUN, "update", "--run-dir", str(run_dir), "--test-id", "CAR-PER-001", "--status", "running")
    invoke(RUN, "update", "--run-dir", str(run_dir), "--test-id", "CAR-PER-001", "--status", "passed", "--actual", "Synthetic save succeeded")

    screenshot = root / "workspace.svg"
    screenshot.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="900" height="560"><rect width="900" height="560" fill="#11131a"/><rect x="60" y="70" width="780" height="420" rx="24" fill="#171a24"/><text x="100" y="145" fill="#fff" font-size="34">Workspace save</text><text x="100" y="205" fill="#ffc247" font-size="22">Evidence-linked persona feedback</text></svg>', encoding="utf-8")
    invoke(
        EVIDENCE, "page", "--run-dir", str(run_dir), "--url", "https://example.test/workspace",
        "--title", "Workspace", "--state", "after save", "--viewport", "1440x900",
        "--test-id", "CAR-PER-001", "--screenshot", str(screenshot),
        "--visual-review", "The synthetic workspace screenshot is legible with no clipping or overlap.",
        *sum((["--unavailable", channel] for channel in ("dom", "accessibility", "console", "network", "performance", "trace", "data", "storage", "video", "other")), []),
    )
    missing = invoke(RUN, "complete", "--run-dir", str(run_dir), ok=False)
    if "planned personas without completed journey feedback" not in missing.stderr or "planned personas without exploratory observations" not in missing.stderr or "exploration charters without observations" not in missing.stderr:
        raise AssertionError("run completion did not expose missing persona or exploration evidence")

    invoke(EVIDENCE, "strategy", "--run-dir", str(run_dir), "--mode", "balanced", "--rationale", "Map broadly, then drill into save recovery.")
    for persona_id, name, role, goal in (
        ("PERSONA-PRIMARY", "Primary user", "workspace owner", "save a change"),
        ("PERSONA-FIRST-TIME", "First-time user", "new collaborator", "understand and complete the flow"),
    ):
        invoke(
            EVIDENCE, "persona", "--run-dir", str(run_dir), "--page-id", "PAGE-001",
            "--persona-id", persona_id, "--name", name, "--role", role, "--goal", goal,
            "--intent", goal, "--plan", "Find the workspace save action", "--plan", "Save realistic content and verify the result",
            "--action", "Opened the workspace", "--action", "Saved a synthetic change", "--outcome", "The change saved and the result was visible.",
            "--test-id", "CAR-PER-001", "--rating", "joy=4", "--rating", "happiness=4", "--rating", "surprise=2", "--rating", "usability=4",
            "--positive", "The saved state is visible beside its evidence.",
            "--joyful-moment", "The confirmation made the saved state immediately clear.",
            "--surprise", "The recovery affordance was less prominent than expected.",
            "--confusion", "It was not obvious how a failed save would be retried.",
            "--friction", "Recovery guidance could be more explicit.",
            "--recommendation", "Keep retry status close to the saved-state confirmation.",
            "--severity", "low", "--confidence", "observed",
        )
    invoke(
        EVIDENCE, "explore", "--run-dir", str(run_dir), "--page-id", "PAGE-001",
        "--persona-id", "PERSONA-PRIMARY",
        "--strategy", "breadth", "--charter-id", "CHARTER-BREADTH", "--charter", "Sample the page, controls, and transitions.",
        "--title", "Workspace surface mapped", "--test-id", "CAR-PER-001", "--action", "Inspect page regions and save affordance",
        "--observation", "The primary save state and adjacent workspace regions were sampled.", "--coverage", "workspace", "--coverage", "save control",
    )
    invoke(
        EVIDENCE, "explore", "--run-dir", str(run_dir), "--page-id", "PAGE-001",
        "--persona-id", "PERSONA-FIRST-TIME",
        "--strategy", "depth", "--charter-id", "CHARTER-DEPTH", "--charter", "Exercise retry and recovery around save.",
        "--title", "Save recovery drilled", "--test-id", "CAR-PER-001", "--action", "Review retry and recovery evidence",
        "--observation", "The saved state remains explicit; interruption behavior needs a live fault-injection adapter.",
        "--next-step", "Run an authorized dependency interruption test.", "--coverage", "recovery",
    )
    invoke(EVIDENCE, "analyze", "--run-dir", str(run_dir))
    invoke(RUN, "complete", "--run-dir", str(run_dir))

    manifest = json.loads((run_dir / "evidence" / "manifest.json").read_text(encoding="utf-8"))
    if manifest["summary"]["personaFeedback"] != 2 or manifest["summary"]["explorationObservations"] != 2:
        raise AssertionError("persona or exploration evidence was not summarized")
    report = (run_dir / "report.html").read_text(encoding="utf-8")
    for required in (
        "Coverage strategy", "Generated user-perspective feedback", "Breadth and depth exploratory testing",
        "Primary user", "First-time user", "Workspace surface mapped", "Save recovery drilled", "rating-grid",
        "Open the live persona journey report", "Open the written journey document",
    ):
        if required not in report:
            raise AssertionError(f"visual report is missing {required!r}")
    journey_html = (run_dir / "persona-journeys.html").read_text(encoding="utf-8")
    journey_markdown = (run_dir / "persona-journeys.md").read_text(encoding="utf-8")
    assert 'class="decision-hero' in journey_html and 'class="evidence-spine"' in journey_html
    assert "Step-level experience is NOT MEASURED" in journey_html
    for required in ("PERSONA JOURNEYS + EXPLORATORY TESTING", "Joyful moments", "Surprises", "Confusion and friction", "4.0"):
        if required not in journey_html:
            raise AssertionError(f"persona journey HTML is missing {required!r}")
    for required in ("# Persona and exploration pass - persona journeys", "Average joy: 4.0 / 5", "Workspace surface mapped"):
        if required not in journey_markdown:
            raise AssertionError(f"persona journey Markdown is missing {required!r}")

    # API/source-only runs still fail closed on persona journeys and can record them without a page.
    api_root = root / "api-only"
    api_root.mkdir()
    api_plan = api_root / "plan.json"
    api_plan.write_text(json.dumps({
        "title": "API-only persona pass", "target": "local API source", "personas": [{"id": "PERSONA-CONSUMER", "name": "API consumer", "goal": "understand an error response"}],
        "explorationCharters": [{"id": "CHARTER-DEPTH", "strategy": "depth", "title": "Explore API errors", "mission": "Follow an error and recovery path."}],
        "tests": [{"id": "CAR-API-001", "title": "API contract", "priority": "P1", "type": "api", "reason": "Check source-backed behavior", "provenance": ["fixture"], "prerequisites": [], "data": ["synthetic request"], "steps": ["Inspect the error contract"], "oracle": "The consumer can understand recovery", "estimatedSeconds": 5, "dependencies": [], "parallelGroup": "api", "adapter": "code-inspection", "flags": {}}],
    }), encoding="utf-8")
    api_run = Path(invoke(RUN, "init", "--root", str(api_root), "--plan", str(api_plan), "--mode", "quick").stdout.strip())
    invoke(RUN, "start", "--run-dir", str(api_run))
    invoke(RUN, "update", "--run-dir", str(api_run), "--test-id", "CAR-API-001", "--status", "running")
    invoke(RUN, "update", "--run-dir", str(api_run), "--test-id", "CAR-API-001", "--status", "passed", "--actual", "The source-defined error is actionable")
    blocked = invoke(
        EVIDENCE, "persona", "--run-dir", str(api_run), "--persona-id", "PERSONA-CONSUMER", "--status", "blocked",
        "--outcome", "Could not validate the authenticated recovery response.", "--missing-input", "a synthetic API account with the consumer role",
    )
    if "questionsForUser" not in blocked.stdout or "a synthetic API account" not in blocked.stdout:
        raise AssertionError("blocked journey did not produce a safe question for the user")
    invoke(
        EVIDENCE, "persona", "--run-dir", str(api_run), "--persona-id", "PERSONA-CONSUMER", "--status", "reviewed", "--intent", "understand an error response",
        "--plan", "Inspect the error schema", "--action", "Traced the documented error response", "--outcome", "The consumer can identify the recovery action.",
        "--positive", "The recovery field is explicit.",
    )
    invoke(
        EVIDENCE, "explore", "--run-dir", str(api_run), "--persona-id", "PERSONA-CONSUMER", "--strategy", "depth",
        "--charter-id", "CHARTER-DEPTH", "--charter", "Follow an error and recovery path.", "--title", "API recovery explored",
        "--action", "Traced invalid input into the consumer-visible response", "--observation", "The source contract exposes an actionable recovery field.",
    )
    invoke(RUN, "complete", "--run-dir", str(api_run))
    if not (api_run / "persona-journeys.html").is_file() or not (api_run / "persona-journeys.md").is_file():
        raise AssertionError("API/source-only run did not generate persona journey reports")

print("PASS: size-scaled intent-led personas, emotional measures, browser and API/source-only exploration, safe input questions, fail-closed completion, and HTML/Markdown journey reporting verified.")
