"""Phase 10 audit-gate enforcement tests + R4 Phase 9 CLI-surface assertions."""
import os
import subprocess
import sys
from pathlib import Path

import pytest

from recoil.pipeline.tools.audit_assertions import (
    assert_audio_on_default_for_narrative,
    assert_every_segment_writes_sidecar,
    assert_generate_cli_dispatches,
    assert_run_overnight_dry_run_completes,
    assert_shots_routes_to_r2v_multi,
    assert_single_shot_sidecar_populated,
    assert_single_shot_tag_derivation,
)

REPO = Path(__file__).resolve().parents[2]


def _run_audit():
    return subprocess.run(
        [
            sys.executable,
            "recoil/pipeline/tools/audit_dispatch.py",
            "--project", "tartarus",
            "--episode", "ep_001",
            "--fixture-plan", "recoil/pipeline/tools/audit_fixtures/audit_plan.json",
        ],
        cwd=REPO,
        env={"PYTHONPATH": str(REPO), **os.environ},
        capture_output=True,
        text=True,
    )


def test_audit_dispatch_exits_zero():
    """Audit gate against canonical fixtures must exit 0 after all phase fixes."""
    result = _run_audit()
    assert result.returncode == 0, f"stderr={result.stderr}\nstdout={result.stdout}"
    assert "PASS" in result.stdout
    assert "FAIL" not in result.stdout or "0 FAIL" in result.stdout


def test_audit_fails_on_unhydrated_ref_token():
    """Sanity: synthetic prompt with @Image{identity_1} trips assertion #3."""
    import pytest
    from recoil.pipeline.tools.audit_assertions import assert_no_unhydrated_ref_tokens
    payload = {"prompt": "Anchor @Image{identity_1} is the protagonist."}
    with pytest.raises(AssertionError, match="unhydrated"):
        assert_no_unhydrated_ref_tokens(payload, "r2v_multi", "seeddance-2.0", shot=None)


# ---------------------------------------------------------------------------
# R4 — Phase 9 CLI-surface assertions (#17–#22) + inverted #10
# ---------------------------------------------------------------------------


def test_audio_on_default_assertion_flips_passes():
    """R4 revert: audio default ON now passes (was off-for-dialogue in R3)."""
    payload = {"generate_audio": True}
    assert_audio_on_default_for_narrative(
        payload, "video_i2v", "seeddance-2.0", {"has_dialogue": True}
    )


def test_audio_on_default_raises_when_off():
    payload = {"generate_audio": False}
    with pytest.raises(AssertionError, match="default must be True"):
        assert_audio_on_default_for_narrative(
            payload, "video_i2v", "seeddance-2.0", {"has_dialogue": True}
        )


def test_audio_on_default_explicit_override_bypasses():
    payload = {"generate_audio": False, "explicit_generate_audio_override": True}
    # Explicit override means the caller deliberately set audio=off; no fire.
    assert_audio_on_default_for_narrative(
        payload, "video_i2v", "seeddance-2.0", {"has_dialogue": True}
    )


def test_single_shot_tag_assertion_solo_jade_passes():
    payload = {"tag": "SOLO_JADE"}
    shot = {"asset_data": {"characters": [{"char_id": "JADE"}]}}
    assert_single_shot_tag_derivation(payload, "video_i2v", "seeddance-2.0", shot)


def test_single_shot_tag_assertion_duo_tag_wrong_for_solo_shot():
    """A solo (1-char) shot tagged DUO_X_Y is structurally wrong — the
    expected prefix is SOLO_, and DUO_X_Y doesn't start with SOLO. This is
    the A3 leak shape the assertion targets."""
    payload = {"tag": "DUO_X_Y"}
    shot = {"asset_data": {"characters": [{"char_id": "JADE"}]}}
    with pytest.raises(AssertionError, match="doesn't match shape"):
        assert_single_shot_tag_derivation(payload, "video_i2v", "seeddance-2.0", shot)


def test_single_shot_tag_assertion_duo_passes():
    payload = {"tag": "DUO_JADE_WREN"}
    shot = {"asset_data": {"characters": [{"char_id": "JADE"}, {"char_id": "WREN"}]}}
    assert_single_shot_tag_derivation(payload, "video_i2v", "seeddance-2.0", shot)


def test_single_shot_tag_assertion_solo_env_passes_for_env_shot():
    payload = {"tag": "SOLO_ENV"}
    shot = {"asset_data": {"characters": []}}
    assert_single_shot_tag_derivation(payload, "video_i2v", "seeddance-2.0", shot)


def test_single_shot_sidecar_hash_format_passes():
    payload = {
        "_audit_sidecar_dict": {
            "provenance": {
                "prompt_engine_version": "abc123def456",
                "refs_used": ["foo.png"],
            }
        },
        "reference_images": ["foo.png"],
    }
    assert_single_shot_sidecar_populated(payload, "video_i2v", "seeddance-2.0", None)


def test_single_shot_sidecar_bad_hash_fails():
    payload = {
        "_audit_sidecar_dict": {
            "provenance": {"prompt_engine_version": "v1", "refs_used": ["foo.png"]}
        },
        "reference_images": ["foo.png"],
    }
    with pytest.raises(AssertionError, match="12-char SHA256 prefix"):
        assert_single_shot_sidecar_populated(
            payload, "video_i2v", "seeddance-2.0", None
        )


def test_single_shot_sidecar_empty_refs_fails():
    payload = {
        "_audit_sidecar_dict": {
            "provenance": {
                "prompt_engine_version": "abc123def456",
                "refs_used": [],
            }
        },
        "reference_images": ["foo.png"],
    }
    with pytest.raises(AssertionError, match="refs_used empty"):
        assert_single_shot_sidecar_populated(
            payload, "video_i2v", "seeddance-2.0", None
        )


def test_shots_flag_routes_to_r2v_multi_passes():
    payload = {"_audit_cli_surface": "shots_flag", "r2v_multi": True}
    assert_shots_routes_to_r2v_multi(payload, "video_i2v", "seeddance-2.0", None)


def test_shots_flag_per_shot_regression_caught():
    payload = {"_audit_cli_surface": "shots_flag", "r2v_multi": False}
    with pytest.raises(AssertionError, match="A5 regression"):
        assert_shots_routes_to_r2v_multi(
            payload, "video_i2v", "seeddance-2.0", None
        )


def test_every_segment_writes_sidecar_passes():
    payload = {
        "r2v_multi": True,
        "_audit_segment_outputs": [
            {"shot_id": "EP001_SH16", "sidecar_present": True},
            {"shot_id": "EP001_SH17", "sidecar_present": True},
        ],
    }
    assert_every_segment_writes_sidecar(
        payload, "r2v_multi", "seeddance-2.0", None
    )


def test_every_segment_writes_sidecar_catches_missing():
    payload = {
        "r2v_multi": True,
        "_audit_segment_outputs": [
            {"shot_id": "EP001_SH16", "sidecar_present": False},
        ],
    }
    with pytest.raises(AssertionError, match="missing sidecar"):
        assert_every_segment_writes_sidecar(
            payload, "r2v_multi", "seeddance-2.0", None
        )


def test_generate_cli_dispatches_passes_on_no_error():
    payload = {"_audit_cli_surface": "generate_cli", "_audit_cli_error": ""}
    assert_generate_cli_dispatches(payload, "video_i2v", "seeddance-2.0", None)


def test_generate_cli_dispatches_catches_b1_regression():
    payload = {
        "_audit_cli_surface": "generate_cli",
        "_audit_cli_error": "raise NotImplementedError('rewire to dispatch')",
    }
    with pytest.raises(AssertionError, match="B1 regression"):
        assert_generate_cli_dispatches(
            payload, "video_i2v", "seeddance-2.0", None
        )


def test_run_overnight_dry_run_passes_on_rc0():
    payload = {"_audit_cli_surface": "run_overnight_dry", "_audit_cli_returncode": 0}
    assert_run_overnight_dry_run_completes(
        payload, "video_i2v", "seeddance-2.0", None
    )


def test_run_overnight_dry_run_catches_nonzero_rc():
    payload = {
        "_audit_cli_surface": "run_overnight_dry",
        "_audit_cli_returncode": 1,
        "_audit_cli_error": "Traceback... PosixPath is not JSON serializable",
    }
    with pytest.raises(AssertionError, match="rc=1"):
        assert_run_overnight_dry_run_completes(
            payload, "video_i2v", "seeddance-2.0", None
        )
