import json

import pytest

from recoil.workspace.board_comments import (
    BoardCommentsCorruptError,
    _comments_path,
    add_comment,
    delete_comment,
    load_comments,
    resolve_comment,
)


@pytest.fixture
def comments_project(tmp_path, monkeypatch):
    projects_root = tmp_path / "projects"
    projects_root.mkdir()
    (projects_root / ".recoil-data-root").write_text("recoil-data-root\n", encoding="utf-8")
    project = "fixture"
    project_root = projects_root / project
    project_root.mkdir()
    (project_root / "project_config.json").write_text("{}", encoding="utf-8")
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(projects_root))
    return project


def test_load_missing_returns_empty_shape(comments_project):
    assert load_comments(comments_project, "EP001") == {
        "schema_version": 1,
        "episode_id": "EP001",
        "comments": [],
    }
    assert not _comments_path(comments_project, "EP001").exists()


def test_add_board_comment_persists(comments_project):
    comment = add_comment(
        comments_project,
        "EP001",
        target_type="board",
        batch_id="BATCH_001",
        segment_id=None,
        body="expand the reveal",
        tag="expand",
    )

    path = _comments_path(comments_project, "EP001")
    assert path.name == "board_comments.json"
    assert path.parent.parts[-3:] == ("prep", "ep_001", "storyboards")
    assert path.is_file()
    assert comment["segment_id"] is None

    reloaded = load_comments(comments_project, "EP001")
    assert reloaded["comments"] == [comment]
    assert reloaded["comments"][0]["id"].startswith("cmt_")
    assert reloaded["comments"][0]["resolved"] is False
    assert reloaded["comments"][0]["resolved_at"] is None


def test_add_panel_comment_requires_segment_id(comments_project):
    with pytest.raises(ValueError):
        add_comment(
            comments_project,
            "EP001",
            target_type="panel",
            batch_id="BATCH_001",
            segment_id=None,
            body="too fast",
            tag="compress",
        )


def test_add_invalid_tag_raises(comments_project):
    with pytest.raises(ValueError):
        add_comment(
            comments_project,
            "EP001",
            target_type="board",
            batch_id="BATCH_001",
            segment_id=None,
            body="needs a different intent",
            tag="bad_tag",
        )


def test_add_missing_panel_tag_on_panel_raises(comments_project):
    with pytest.raises(ValueError):
        add_comment(
            comments_project,
            "EP001",
            target_type="panel",
            batch_id="BATCH_001",
            segment_id="EP001_SH02",
            body="missing beat",
            tag="missing_panel",
        )

    comment = add_comment(
        comments_project,
        "EP001",
        target_type="board",
        batch_id="BATCH_001",
        segment_id=None,
        body="add a missing panel here",
        tag="missing_panel",
    )
    assert comment["tag"] == "missing_panel"
    assert comment["target_type"] == "board"


@pytest.mark.parametrize("batch_id", ["", None])
def test_add_empty_batch_id_raises(comments_project, batch_id):
    with pytest.raises(ValueError):
        add_comment(
            comments_project,
            "EP001",
            target_type="board",
            batch_id=batch_id,
            segment_id=None,
            body="cannot attach",
            tag="note",
        )


def test_resolve_comment_flips_state(comments_project):
    comment = add_comment(
        comments_project,
        "EP001",
        target_type="panel",
        batch_id="BATCH_001",
        segment_id="EP001_SH02",
        body="flow issue",
        tag="flow",
    )

    resolved = resolve_comment(comments_project, "EP001", comment["id"])

    assert resolved is not None
    assert resolved["resolved"] is True
    assert isinstance(resolved["resolved_at"], str)
    assert load_comments(comments_project, "EP001")["comments"][0] == resolved
    assert resolve_comment(comments_project, "EP001", "cmt_" + "0" * 32) is None


def test_delete_comment_removes_record(comments_project):
    keep = add_comment(
        comments_project,
        "EP001",
        target_type="board",
        batch_id="BATCH_001",
        segment_id=None,
        body="keep me",
        tag="note",
    )
    target = add_comment(
        comments_project,
        "EP001",
        target_type="panel",
        batch_id="BATCH_001",
        segment_id="EP001_SH02",
        body="delete me",
        tag="flow",
    )

    removed = delete_comment(comments_project, "EP001", target["id"])

    assert removed is not None
    assert removed["id"] == target["id"]
    remaining = load_comments(comments_project, "EP001")["comments"]
    assert remaining == [keep]
    # idempotent / unknown id → None, no mutation
    assert delete_comment(comments_project, "EP001", target["id"]) is None
    assert delete_comment(comments_project, "EP001", "cmt_" + "0" * 32) is None
    assert load_comments(comments_project, "EP001")["comments"] == [keep]


def test_episode_id_normalization(comments_project):
    comment = add_comment(
        comments_project,
        "1",
        target_type="board",
        batch_id="BATCH_001",
        segment_id=None,
        body="same episode",
        tag="note",
    )

    assert load_comments(comments_project, "EP001")["comments"] == [comment]
    assert _comments_path(comments_project, "1") == _comments_path(comments_project, "EP001")


def test_corrupt_file_raises_and_is_preserved(comments_project):
    path = _comments_path(comments_project, "EP001")
    path.parent.mkdir(parents=True)
    raw = b"{bad"
    path.write_bytes(raw)

    with pytest.raises(BoardCommentsCorruptError):
        load_comments(comments_project, "EP001")
    with pytest.raises(BoardCommentsCorruptError):
        add_comment(
            comments_project,
            "EP001",
            target_type="board",
            batch_id="BATCH_001",
            segment_id=None,
            body="do not clobber",
            tag="note",
        )
    assert path.read_bytes() == raw


def test_unknown_schema_version_raises(comments_project):
    _write_comments_payload(comments_project, {"schema_version": 99, "episode_id": "EP001", "comments": []})

    with pytest.raises(BoardCommentsCorruptError):
        load_comments(comments_project, "EP001")


def test_missing_schema_version_raises(comments_project):
    _write_comments_payload(comments_project, {"episode_id": "EP001", "comments": []})

    with pytest.raises(BoardCommentsCorruptError):
        load_comments(comments_project, "EP001")


def test_missing_comments_key_raises(comments_project):
    _write_comments_payload(comments_project, {"schema_version": 1, "episode_id": "EP001"})

    with pytest.raises(BoardCommentsCorruptError):
        load_comments(comments_project, "EP001")


def test_non_list_comments_raises(comments_project):
    payload = {"schema_version": 1, "episode_id": "EP001", "comments": {}}
    path = _write_comments_payload(comments_project, payload)
    raw = path.read_bytes()

    with pytest.raises(BoardCommentsCorruptError):
        load_comments(comments_project, "EP001")
    with pytest.raises(BoardCommentsCorruptError):
        add_comment(
            comments_project,
            "EP001",
            target_type="board",
            batch_id="BATCH_001",
            segment_id=None,
            body="do not clobber",
            tag="note",
        )
    assert path.read_bytes() == raw


def test_mismatched_episode_id_raises_and_is_preserved(comments_project):
    path = _write_comments_payload(
        comments_project,
        {"schema_version": 1, "episode_id": "EP002", "comments": []},
    )
    raw = path.read_bytes()

    with pytest.raises(BoardCommentsCorruptError):
        load_comments(comments_project, "EP001")
    with pytest.raises(BoardCommentsCorruptError):
        add_comment(
            comments_project,
            "EP001",
            target_type="board",
            batch_id="BATCH_001",
            segment_id=None,
            body="do not mix",
            tag="note",
        )
    assert path.read_bytes() == raw


@pytest.mark.parametrize(
    "mutate",
    [
        pytest.param(
            lambda payload: payload["comments"].__setitem__(
                0,
                {"id": "cmt_x", "target_type": "panel", "batch_id": "BATCH_001"},
            ),
            id="minimal-bad-record",
        ),
        pytest.param(lambda payload: payload["comments"][0].pop("author"), id="missing-author"),
        pytest.param(lambda payload: payload["comments"][0].__setitem__("author", None), id="non-str-author"),
        pytest.param(lambda payload: payload["comments"][0].__setitem__("resolved", True), id="resolved-missing-at"),
        pytest.param(lambda payload: payload["comments"][0].pop("resolved_at"), id="missing-resolved-at"),
        pytest.param(lambda payload: payload["comments"][0].pop("segment_id"), id="missing-segment-id"),
        pytest.param(lambda payload: payload.pop("episode_id"), id="missing-top-episode-id"),
        pytest.param(lambda payload: payload.__setitem__("episode_id", 1), id="non-str-top-episode-id"),
    ],
)
def test_corrupt_comment_record_raises_and_is_preserved(comments_project, mutate):
    payload = _valid_payload()
    mutate(payload)
    path = _write_comments_payload(comments_project, payload)
    raw = path.read_bytes()

    with pytest.raises(BoardCommentsCorruptError):
        load_comments(comments_project, "EP001")
    with pytest.raises(BoardCommentsCorruptError):
        add_comment(
            comments_project,
            "EP001",
            target_type="board",
            batch_id="BATCH_001",
            segment_id=None,
            body="do not clobber",
            tag="note",
        )
    assert path.read_bytes() == raw


def _write_comments_payload(project: str, payload: dict):
    path = _comments_path(project, "EP001")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
    return path


def _valid_payload() -> dict:
    return {
        "schema_version": 1,
        "episode_id": "EP001",
        "comments": [
            {
                "id": "cmt_" + "1" * 32,
                "target_type": "panel",
                "batch_id": "BATCH_001",
                "segment_id": "EP001_SH02",
                "body": "tighten this",
                "tag": "compress",
                "author": "JT",
                "created_at": "2026-06-18T00:00:00Z",
                "resolved": False,
                "resolved_at": None,
            }
        ],
    }
