"""Integration smoke test — validates the full HTTP → disk → BUS path.

Uses TestClient(app) so the test runs in-process without requiring the
LaunchAgent or uvicorn to be active. NEVER consume the SSE stream —
TestClient deadlocks on EventSourceResponse. Assert BUS.history() instead.
"""
from __future__ import annotations

from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from recoil.api.eventbus import BUS
from recoil.api.main import app
from recoil.api.stub_routes import _reset_acted_for_tests


@pytest.fixture
def client():
    """TestClient bound to lifespan so BUS binds to the test loop."""
    _reset_acted_for_tests()
    BUS._reset_for_tests()
    with TestClient(app) as c:
        yield c


def test_system_status_returns_200(client: TestClient) -> None:
    """API starts and system-status endpoint responds."""
    r = client.get("/api/system-status")
    assert r.status_code == 200
    data = r.json()
    assert "apiVersion" in data or "schemaVersion" in data or len(data) > 0


def test_create_proposal_returns_ok(client: TestClient) -> None:
    """POST /api/chat/proposals creates a proposal with the new kind field."""
    body = {
        "target": "shot:TEST_BEAT_001",
        "title": "Smoke test proposal",
        "est_cost_usd": 0.01,
        "est_time": "1s",
        "kind": "PromptRewriteProposal",
        "diff": [{"kind": "rewrite", "after": "New prompt text here"}],
    }
    r = client.post("/api/chat/proposals", json=body)
    assert r.status_code == 200, r.text
    data = r.json()
    assert data.get("ok") is True
    assert "id" in data
    # Kind field must round-trip: list proposals and verify kind stored
    proposal_id = data["id"]
    list_r = client.get("/api/chat/proposals/default")
    assert list_r.status_code == 200
    proposals = list_r.json()
    match = next((p for p in proposals if p["id"] == proposal_id), None)
    assert match is not None, f"proposal {proposal_id} not in list"
    assert match.get("kind") == "PromptRewriteProposal"


def test_prompt_rewrite_approve_writes_disk(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a PromptRewriteProposal → shot JSON gets prompt_override field."""
    import json as _json
    from recoil.api.adapters import beats as _beats

    # Seed a minimal shot JSON in tmp_path under a fake project.
    fake_project = "smoke_test_project"
    shots_dir = tmp_path / fake_project / "state" / "visual" / "shots"
    shots_dir.mkdir(parents=True)
    beat_id = "SMOKE_SH01"
    shot_data = {"shot_id": beat_id, "takes": [], "status": "pending"}
    (shots_dir / f"{beat_id}.json").write_text(
        _json.dumps(shot_data), encoding="utf-8"
    )

    # Monkeypatch _shots_dir so beats adapter finds the fake shot.
    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: tmp_path / pid / "state" / "visual" / "shots")

    # Create a PromptRewriteProposal.
    create_body = {
        "target": f"beat:{beat_id}",
        "title": "Smoke: rewrite prompt",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "PromptRewriteProposal",
        "project": fake_project,
        "diff": [{"kind": "rewrite", "after": "A completely new prompt text."}],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    # Approve the proposal.
    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"
    assert result.get("ok") is True

    # Assert disk mutation: shot JSON now has prompt_override.
    shot_path = shots_dir / f"{beat_id}.json"
    updated = _json.loads(shot_path.read_text())
    assert updated.get("prompt_override") == "A completely new prompt text."

    # Assert BUS history has the prompt_rewrite_applied event.
    history = BUS.history()
    rewrite_events = [e for e in history if "prompt_rewrite_applied" in e.summary]
    assert len(rewrite_events) >= 1, f"No prompt_rewrite_applied event; history: {[e.summary for e in history]}"


def test_prompt_rewrite_bad_beat_id_returns_404(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a PromptRewriteProposal with a nonexistent beat → 404 + BUS error event."""
    from recoil.api.adapters import beats as _beats

    # Monkeypatch to an empty directory so no shots exist.
    empty_shots = tmp_path / "empty" / "state" / "visual" / "shots"
    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: empty_shots)

    create_body = {
        "target": "beat:NONEXISTENT_BEAT",
        "title": "Bad beat test",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "PromptRewriteProposal",
        "diff": [{"kind": "rewrite", "after": "irrelevant"}],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": "default"},
    )
    assert ar.status_code == 404, ar.text
    detail = ar.json().get("detail", {})
    assert detail.get("error") == "beat_not_found"
    assert "NONEXISTENT_BEAT" in str(detail)

    # Assert BUS failure event.
    history = BUS.history()
    failure_events = [e for e in history if e.severity == "failure"]
    assert len(failure_events) >= 1
    assert any("prompt_rewrite_target_not_found" in e.summary for e in failure_events)


def test_param_tweak_approve_updates_take(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a ParameterChangeProposal → take dict gets updated fields."""
    import json as _json
    from recoil.api.adapters import beats as _beats

    fake_project = "smoke_pt_project"
    shots_dir = tmp_path / fake_project / "state" / "visual" / "shots"
    shots_dir.mkdir(parents=True)
    beat_id = "SMOKE_PT01"
    take_id = "SMOKE_PT01_T001"
    shot_data = {
        "shot_id": beat_id,
        "takes": [{"take_id": take_id, "status": "pending", "seed": 42}],
    }
    (shots_dir / f"{beat_id}.json").write_text(_json.dumps(shot_data), encoding="utf-8")

    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: tmp_path / pid / "state" / "visual" / "shots")

    create_body = {
        "target": f"take:{take_id}",
        "title": "Smoke: param tweak",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "ParameterChangeProposal",
        "project": fake_project,
        "diff": [{"kind": "param", "key": "seed", "after": 99}],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"
    assert result.get("ok") is True

    updated = _json.loads((shots_dir / f"{beat_id}.json").read_text())
    take = next((t for t in updated.get("takes", []) if t.get("take_id") == take_id), None)
    assert take is not None
    assert take.get("seed") == 99

    history = BUS.history()
    tweak_events = [e for e in history if "param_tweak_applied" in e.summary]
    assert len(tweak_events) >= 1


def test_script_edit_approve_writes_script(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a ScriptEditProposal → script.fountain file is rewritten."""
    import recoil.api.executors.script_edit as _script_edit_mod

    fake_project = "smoke_se_project"
    ep_id = "ep_001"
    ep_dir = tmp_path / fake_project / "episodes" / ep_id
    ep_dir.mkdir(parents=True)
    script_file = ep_dir / "script.fountain"
    script_file.write_text("Title: Original\n\nFADE IN:", encoding="utf-8")

    monkeypatch.setattr(_script_edit_mod, "projects_root", lambda: tmp_path)

    create_body = {
        "target": f"episode:{ep_id}",
        "title": "Smoke: script edit",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "ScriptEditProposal",
        "project": fake_project,
        "diff": [{"kind": "rewrite", "after": "Title: Rewritten\n\nFADE IN:\nINT. NEW SCENE - DAY"}],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"
    assert result.get("ok") is True

    updated_text = script_file.read_text(encoding="utf-8")
    assert "Rewritten" in updated_text
    assert "Original" not in updated_text

    history = BUS.history()
    edit_events = [e for e in history if "script_edit_applied" in e.summary]
    assert len(edit_events) >= 1


def test_beat_insertion_creates_new_shot(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a BeatInsertionProposal → new shot JSON file created."""
    import json as _json
    from recoil.api.adapters import beats as _beats

    fake_project = "smoke_bi_project"
    shots_dir = tmp_path / fake_project / "state" / "visual" / "shots"
    shots_dir.mkdir(parents=True)
    # Seed one existing beat so _next_beat_id increments from it.
    existing = {"shot_id": "EP001_SH01", "episode_id": "EP001", "takes": [], "status": "pending"}
    (shots_dir / "EP001_SH01.json").write_text(_json.dumps(existing), encoding="utf-8")

    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: tmp_path / pid / "state" / "visual" / "shots")

    create_body = {
        "target": "episode:EP001",
        "title": "Smoke: insert beat",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "BeatInsertionProposal",
        "project": fake_project,
        "diff": [
            {"kind": "insert", "key": "text", "text": "A new establishing shot of the city at dawn"},
            {"kind": "insert", "key": "afterBeatId", "after": "EP001_SH01"},
        ],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"
    assert result.get("ok") is True

    # New shot file should exist with the next sequential ID.
    new_path = shots_dir / "EP001_SH02.json"
    assert new_path.exists(), f"Expected {new_path} to exist"
    new_shot = _json.loads(new_path.read_text())
    assert new_shot["shot_id"] == "EP001_SH02"
    assert new_shot["episode_id"] == "EP001"
    assert new_shot["prompt_override"] == "A new establishing shot of the city at dawn"
    assert new_shot["inserted_after"] == "EP001_SH01"
    assert new_shot["status"] == "pending"

    history = BUS.history()
    insert_events = [e for e in history if "beat_insertion_applied" in e.summary]
    assert len(insert_events) >= 1


def test_multi_beat_directive_applies_to_all(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a MultiBeatDirectiveProposal → all target beats get the directive."""
    import json as _json
    from recoil.api.adapters import beats as _beats

    fake_project = "smoke_mbd_project"
    shots_dir = tmp_path / fake_project / "state" / "visual" / "shots"
    shots_dir.mkdir(parents=True)
    for i in (1, 3, 5):
        bid = f"EP001_SH{i:02d}"
        data = {"shot_id": bid, "episode_id": "EP001", "takes": [], "status": "pending"}
        (shots_dir / f"{bid}.json").write_text(_json.dumps(data), encoding="utf-8")

    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: tmp_path / pid / "state" / "visual" / "shots")

    create_body = {
        "target": "episode:EP001",
        "title": "Smoke: multi-beat directive",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "MultiBeatDirectiveProposal",
        "project": fake_project,
        "diff": [
            {"kind": "directive", "key": "beatIds", "after": ["EP001_SH01", "EP001_SH03", "EP001_SH05"]},
            {"kind": "directive", "key": "note", "text": "Increase visual tension"},
        ],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"

    for i in (1, 3, 5):
        bid = f"EP001_SH{i:02d}"
        updated = _json.loads((shots_dir / f"{bid}.json").read_text())
        assert "Increase visual tension" in updated.get("directives", [])

    history = BUS.history()
    dir_events = [e for e in history if "multi_beat_directive_applied" in e.summary]
    assert len(dir_events) >= 1


def test_extract_cutaway_creates_new_shot(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve an ExtractCutawayProposal → new cutaway shot + source marked."""
    import json as _json
    from recoil.api.adapters import beats as _beats

    fake_project = "smoke_ec_project"
    shots_dir = tmp_path / fake_project / "state" / "visual" / "shots"
    shots_dir.mkdir(parents=True)
    source_beat = "EP001_SH05"
    source_data = {
        "shot_id": source_beat,
        "episode_id": "EP001",
        "takes": [],
        "status": "pending",
    }
    (shots_dir / f"{source_beat}.json").write_text(
        _json.dumps(source_data), encoding="utf-8"
    )

    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: tmp_path / pid / "state" / "visual" / "shots")

    create_body = {
        "target": f"beat:{source_beat}",
        "title": "Smoke: extract cutaway",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "ExtractCutawayProposal",
        "project": fake_project,
        "diff": [{"kind": "cutaway", "text": "Close-up of the sealed envelope"}],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"

    # New cutaway file exists.
    cutaway_path = shots_dir / "EP001_SH05_CUT01.json"
    assert cutaway_path.exists()
    cutaway = _json.loads(cutaway_path.read_text())
    assert cutaway["shot_id"] == "EP001_SH05_CUT01"
    assert cutaway["cutaway_source"] == source_beat
    assert cutaway["prompt_override"] == "Close-up of the sealed envelope"
    assert cutaway["is_coverage"] is True

    # Source beat updated with cutaways list.
    source_updated = _json.loads((shots_dir / f"{source_beat}.json").read_text())
    assert "EP001_SH05_CUT01" in source_updated.get("cutaways", [])

    history = BUS.history()
    cut_events = [e for e in history if "extract_cutaway_applied" in e.summary]
    assert len(cut_events) >= 1


def test_ref_swap_writes_override(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a RefSwapProposal → shot JSON gets ref_overrides field."""
    import json as _json
    from recoil.api.adapters import beats as _beats

    fake_project = "smoke_rs_project"
    shots_dir = tmp_path / fake_project / "state" / "visual" / "shots"
    shots_dir.mkdir(parents=True)
    beat_id = "EP001_SH02"
    shot_data = {"shot_id": beat_id, "takes": [], "status": "pending"}
    (shots_dir / f"{beat_id}.json").write_text(
        _json.dumps(shot_data), encoding="utf-8"
    )

    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: tmp_path / pid / "state" / "visual" / "shots")

    create_body = {
        "target": f"beat:{beat_id}",
        "title": "Smoke: ref swap",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "RefSwapProposal",
        "project": fake_project,
        "diff": [
            {"kind": "swap", "before": "sadie_hero.png", "after": "sadie_front.png"},
            {"kind": "promptAdd", "text": "Profile angle, dramatic side lighting"},
        ],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"

    updated = _json.loads((shots_dir / f"{beat_id}.json").read_text())
    assert len(updated.get("ref_overrides", [])) == 1
    assert updated["ref_overrides"][0]["before"] == "sadie_hero.png"
    assert updated["ref_overrides"][0]["after"] == "sadie_front.png"
    assert "Profile angle, dramatic side lighting" in updated.get("prompt_additions", [])

    history = BUS.history()
    swap_events = [e for e in history if "ref_swap_applied" in e.summary]
    assert len(swap_events) >= 1


def test_retry_strategy_edit_pins_strategy(
    client: TestClient, tmp_path: Path, monkeypatch
) -> None:
    """Approve a RetryStrategyEditProposal → shot JSON gets pinned_strategy."""
    import json as _json
    from recoil.api.adapters import beats as _beats

    fake_project = "smoke_rse_project"
    shots_dir = tmp_path / fake_project / "state" / "visual" / "shots"
    shots_dir.mkdir(parents=True)
    beat_id = "EP001_SH02"
    shot_data = {"shot_id": beat_id, "takes": [], "status": "pending"}
    (shots_dir / f"{beat_id}.json").write_text(
        _json.dumps(shot_data), encoding="utf-8"
    )

    monkeypatch.setattr(_beats, "_shots_dir", lambda pid: tmp_path / pid / "state" / "visual" / "shots")

    create_body = {
        "target": f"beat:{beat_id}",
        "title": "Smoke: pin retry strategy",
        "est_cost_usd": 0.0,
        "est_time": "instant",
        "kind": "RetryStrategyEditProposal",
        "project": fake_project,
        "diff": [
            {"kind": "strategy", "key": "name", "after": "add_turnaround_angles"},
            {"kind": "strategy", "key": "rationale", "text": "Proven winner for identity drift"},
        ],
    }
    cr = client.post("/api/chat/proposals", json=create_body)
    assert cr.status_code == 200, cr.text
    proposal_id = cr.json()["id"]

    ar = client.post(
        f"/api/chat/proposals/{proposal_id}/approve",
        json={"project": fake_project},
    )
    assert ar.status_code == 200, ar.text
    result = ar.json()
    assert result.get("status") == "executed"

    updated = _json.loads((shots_dir / f"{beat_id}.json").read_text())
    pinned = updated.get("pinned_strategy")
    assert pinned is not None
    assert pinned["name"] == "add_turnaround_angles"
    assert pinned["rationale"] == "Proven winner for identity drift"

    history = BUS.history()
    strat_events = [e for e in history if "retry_strategy_edit_applied" in e.summary]
    assert len(strat_events) >= 1
