import json

from autoshorts.claims.fact_gate import Claim, ClaimType, validate_claims_for_candidate
from autoshorts.cli.demo_review_batch_v2 import build_demo_review_batch_v2, main
from autoshorts.compliance.ai_disclosure import (
    AIDisclosureReview,
    DisclosureValue,
    SyntheticMediaType,
    validate_ai_disclosure_review,
)
from autoshorts.content.to_candidate_v2 import ScriptInputV2, convert_to_candidate_v2
from autoshorts.review.approvals_v2 import parse_approval_command_v2
from autoshorts.review.workflow_v2 import build_review_batch_v2, classify_candidate_v2
from autoshorts.strategy.spine_v2 import (
    AIDisclosureDecision,
    AudienceNeed,
    ClaimRisk,
    ContentBriefV2,
    ExperimentHypothesis,
    HumanTexture,
    SaturationRisk,
    StrategyScoreV2,
    VideoCandidateV2,
    ViewerEmotion,
    build_demo_candidates_v2,
    build_review_package_v2,
    render_review_package_v2,
    validate_candidate_v2,
)


def hypothesis() -> ExperimentHypothesis:
    return ExperimentHypothesis("Tests whether visible fail examples drive saves.", "saves_per_view")


def brief(**overrides) -> ContentBriefV2:
    data = dict(
        brief_id="meeting-summary-brief",
        series_id="ai_output_autopsy",
        topic="Meeting summary misses decision target",
        audience_need=AudienceNeed.CLARITY,
        job_to_be_done="When I summarize a meeting, I want decision evidence, so I can act.",
        viewer_identity="office_worker",
        emotion_before=ViewerEmotion.CONFUSED,
        emotion_after=ViewerEmotion.IN_CONTROL,
        audience_problem="Generic AI summary misses the decision.",
        payoff="Ask for decision evidence.",
        search_intent_phrase="better meeting summary prompt",
        saturation_risk=SaturationRisk.MEDIUM,
        human_texture=HumanTexture.MINI_CASE,
        claim_risk=ClaimRisk.LOW,
        ai_disclosure_required=AIDisclosureDecision.NO,
        experiment_hypothesis=hypothesis(),
        platforms=("youtube_shorts", "tiktok", "instagram_reels", "linkedin_manual"),
        target_duration_seconds=38,
    )
    data.update(overrides)
    return ContentBriefV2(**data)


def script(**overrides) -> ScriptInputV2:
    data = dict(
        title="Your AI summary has no decision target",
        hook="Your AI summary is useless because it has no decision target.",
        first_frame="Split-screen: transcript vs. useless summary stamped USELESS.",
        visible_fail="The AI summary says great discussion but misses the launch decision.",
        anti_example="A useless AI summary that says great discussion and lists no decision, owner, or blocker.",
        mechanism="AI compresses everything equally when no decision target exists.",
        fix="Name the decision and ask for blockers, owners, risks, and next steps.",
        before_after="Before: generic summary. After: decision evidence.",
        takeaway="Don't ask for a summary. Ask for decision evidence.",
        save_reason="Viewer can copy the decision-evidence sentence.",
        share_reason="Every office worker knows bad meeting summaries.",
        follow_reason="Follow for small systems that make digital work clearer.",
        human_texture_note="messy meeting transcript with launch decision",
        claims=(Claim("Decision-targeted summaries preserve relevance better than generic summaries.", ClaimType.OPINION, risk=ClaimRisk.LOW),),
        platform_fit={"youtube_shorts": "strong", "tiktok": "strong", "instagram_reels": "medium", "linkedin_manual": "strong silent-autoplay fit"},
        linkedin_title="A better way to brief AI meeting summaries",
        linkedin_caption="Most AI meeting summaries fail because they protect no decision. Name the decision first; then ask for blockers, owners, risks, and next steps.",
        risks=("medium saturation",),
        visual_structure="motion-led compression funnel with visible transcript conflict",
        render_backend_candidate="python_motion",
    )
    data.update(overrides)
    return ScriptInputV2(**data)


def candidate(**overrides) -> VideoCandidateV2:
    return convert_to_candidate_v2(brief(), script(**overrides))


def test_to_candidate_v2_creates_valid_candidate_with_linkedin_manual_fit():
    cand = candidate()

    assert cand.candidate_id == "ai-output-autopsy-your-ai-summary-has-no-decision-target-38s"
    assert "linkedin_manual" in cand.platforms
    assert cand.linkedin_caption.startswith("Most AI meeting summaries")
    assert validate_candidate_v2(cand).is_valid


def test_workflow_v2_classifies_ready_blocked_and_invalid_candidates():
    ready = candidate()
    blocked = candidate(human_texture_note="")
    invalid = candidate(series_id_override="not-real")

    assert classify_candidate_v2(ready).status == "ready_for_review"
    blocked_item = classify_candidate_v2(blocked)
    assert blocked_item.status == "blocked"
    assert "human_texture_note is required" in blocked_item.reasons
    invalid_item = classify_candidate_v2(invalid)
    assert invalid_item.status == "invalid"
    assert any("unknown series_id" in reason for reason in invalid_item.reasons)

    batch = build_review_batch_v2((ready, blocked, invalid))
    assert len(batch.ready_for_review) == 1
    assert len(batch.blocked) == 1
    assert len(batch.invalid) == 1


def test_review_package_v2_json_is_deterministic_and_roundtrips():
    package = build_review_package_v2(candidate())

    first = package.to_json()
    second = package.to_json()
    loaded = json.loads(first)

    assert first == second
    assert list(loaded.keys())[:4] == ["candidate_id", "version", "script_hash", "media_hash"]
    assert loaded["linkedin_caption"].startswith("Most AI meeting summaries")
    assert package == package.from_dict(loaded)


def test_approval_parser_v2_requires_exact_candidate_version_and_hash():
    package = build_review_package_v2(candidate())

    assert parse_approval_command_v2(package.approval_command, package).action == "approve"
    assert parse_approval_command_v2(package.approval_command.replace(package.script_hash, "badbadbad"), package).action == "blocked"
    assert parse_approval_command_v2("APPROVE missing v2.0.0 abc", package).action == "blocked"
    assert parse_approval_command_v2("passt schon", package).action == "needs_clarification"


def test_score_v2_is_calibrated_and_not_automatically_100():
    solid = candidate()
    score = StrategyScoreV2.from_candidate(solid)

    assert 75 <= score.total <= 93
    assert score.breakdown["human_texture"].points < score.breakdown["human_texture"].max_points

    weak_texture = StrategyScoreV2.from_candidate(candidate(human_texture=HumanTexture.SOURCE_BASED_CLAIM, human_texture_note="source-based claim only"))
    generic = StrategyScoreV2.from_candidate(candidate(hook="This AI tool will change your life."))
    saturated = StrategyScoreV2.from_candidate(candidate(saturation_risk=SaturationRisk.HIGH))
    weak_frame = StrategyScoreV2.from_candidate(candidate(first_frame="Plain title card with text only"))

    assert weak_texture.total < score.total
    assert generic.total < score.total
    assert saturated.total < score.total
    assert weak_frame.total < score.total
    assert "First Frame" in render_review_package_v2(build_review_package_v2(solid))
    assert "Score Breakdown:" in render_review_package_v2(build_review_package_v2(solid))


def test_anti_example_required_for_autopsy_teardown_red_flags_and_decision_design():
    for series_id in ("ai_output_autopsy", "workflow_teardown", "digital_red_flags", "decision_design"):
        cand = convert_to_candidate_v2(brief(series_id=series_id), script(anti_example=""))
        result = validate_candidate_v2(cand)
        assert not result.is_valid
        assert "anti_example is required for this series" in result.errors

    attention = convert_to_candidate_v2(
        brief(series_id="attention_traps"),
        script(anti_example="", concrete_scene="phone opened for one task, feed redirects attention"),
    )
    assert validate_candidate_v2(attention).is_valid


def test_human_texture_note_is_required_when_human_texture_is_not_none():
    result = validate_candidate_v2(candidate(human_texture_note=""))

    assert not result.is_valid
    assert "human_texture_note is required" in result.errors


def test_claim_gate_blocks_sourced_and_risky_claims_without_source_but_allows_opinion():
    assert validate_claims_for_candidate(candidate(claims=(Claim("This is a practical rule.", ClaimType.OPINION, risk=ClaimRisk.LOW),))).is_valid

    sourced = validate_claims_for_candidate(candidate(claims=(Claim("Platform says X.", ClaimType.SOURCED_FACT, risk=ClaimRisk.MEDIUM),)))
    risky = validate_claims_for_candidate(candidate(claims=(Claim("This prevents scams.", ClaimType.RISKY_CLAIM, risk=ClaimRisk.HIGH),)))
    experience = validate_claims_for_candidate(candidate(human_texture_note="", claims=(Claim("We tested this.", ClaimType.EXPERIENCE, risk=ClaimRisk.LOW),)))

    assert "source is required for sourced_fact" in sourced.errors
    assert "source is required for risky_claim" in risky.errors
    assert "high risk claim requires source" in risky.errors
    assert "experience claim requires human_texture_note" in experience.errors


def test_ai_disclosure_unclear_blocks_and_realistic_media_requires_manual_review():
    unclear = AIDisclosureReview(
        youtube=DisclosureValue.UNCLEAR,
        tiktok=DisclosureValue.NO,
        instagram=DisclosureValue.NO,
        linkedin=DisclosureValue.NO,
        reason="unclear synthetic background",
        synthetic_media_type=SyntheticMediaType.UNCLEAR,
    )
    realistic = AIDisclosureReview(
        youtube=DisclosureValue.YES,
        tiktok=DisclosureValue.YES,
        instagram=DisclosureValue.YES,
        linkedin=DisclosureValue.YES,
        reason="realistic generated person",
        synthetic_media_type=SyntheticMediaType.REALISTIC_PERSON,
        manual_review_required=False,
    )
    text_only = AIDisclosureReview.text_only_assistance("script and captions only")

    assert not validate_ai_disclosure_review(unclear).is_valid
    assert "unclear disclosure blocks rendering" in validate_ai_disclosure_review(unclear).errors
    assert not validate_ai_disclosure_review(realistic).is_valid
    assert "realistic synthetic media requires manual_review_required" in validate_ai_disclosure_review(realistic).errors
    assert validate_ai_disclosure_review(text_only).is_valid


def test_demo_review_batch_v2_limits_to_three_candidates_and_cli_outputs_json(capsys):
    batch = build_demo_review_batch_v2(limit=3)
    assert len(batch.ready_for_review) <= 3

    exit_code = main(["--json"], stdout=None)
    captured = capsys.readouterr().out

    assert exit_code == 0
    payload = json.loads(captured)
    assert len(payload["ready_for_review"]) <= 3
    assert payload["side_effects"] == []
    assert payload["external_calls"] == []
