"""M1 board behavior: the board read (courts + balls, ordered), the ball
PATCH with the status<->review FIELD coupling (review-patterns-ref §2.1/2.2
— field coupling only; touch/movement automations are M2), the serve POST,
and /api/meta serving the one vocabulary authority."""

from app import constants
from app.models import Ball, Court, Match, Player, Side, TeamMember, TeamNode, Touch
from tests.conftest import make_player


def _board(db):
    """A small two-court board: rian + erin on a match, pages court with two
    balls (positions 1, 0 — proves ordering), issues court with one assigned
    ball. Returns a dict of the rows."""
    rian = make_player(db, "rian", display_name="Rian")
    erin = make_player(db, "erin", display_name="Erin")

    match = Match(slug="testmatch", name="Test Match")
    db.add(match)
    db.flush()
    side = Side(match_id=match.id, name="Bowden Works", position=1)
    db.add(side)
    db.flush()
    node = TeamNode(side_id=side.id, name="Front line", boundary_player_id=rian.id)
    db.add(node)
    db.flush()
    db.add(TeamMember(match_id=match.id, node_id=node.id, player_id=rian.id))
    db.add(TeamMember(match_id=match.id, node_id=node.id, player_id=erin.id))

    pages = Court(
        match_id=match.id,
        config_key="pages",
        name="Pages",
        position=0,
        config=constants.COURT_CONFIGS["pages"],
    )
    issues = Court(
        match_id=match.id,
        config_key="issues",
        name="Issues",
        position=1,
        config=constants.COURT_CONFIGS["issues"],
    )
    db.add_all([pages, issues])
    db.flush()

    second = Ball(
        court_id=pages.id, title="About", status="open", review_state="not ready", position=1
    )
    first = Ball(
        court_id=pages.id,
        title="Home",
        status="open",
        review_state="not ready",
        tags=["main"],
        position=0,
    )
    issue = Ball(
        court_id=issues.id,
        title="Fix the footer",
        status="Open",
        category="Revision",
        action_to=[erin.id],
        author_id=rian.id,
        position=0,
    )
    db.add_all([second, first, issue])
    db.commit()
    return {
        "rian": rian,
        "erin": erin,
        "match": match,
        "pages": pages,
        "issues": issues,
        "first": first,
        "second": second,
        "issue": issue,
    }


# ------------------------------------------------------------------- board


def test_board_returns_courts_and_ordered_balls(authed_client, db_session):
    rows = _board(db_session)
    resp = authed_client.get("/api/matches/testmatch/board")
    assert resp.status_code == 200
    body = resp.json()

    assert body["match"] == {"slug": "testmatch", "name": "Test Match"}
    assert [p["username"] for p in body["players"]] == ["erin", "rian"]
    assert body["players"][1]["display_name"] == "Rian"

    assert [c["config_key"] for c in body["courts"]] == ["pages", "issues"]
    pages_court = body["courts"][0]
    assert pages_court["config"]["columns"] == constants.COURT_CONFIGS["pages"]["columns"]
    # balls ordered by position, not insert order
    assert [b["title"] for b in pages_court["balls"]] == ["Home", "About"]
    assert pages_court["balls"][0]["tags"] == ["main"]

    issue = body["courts"][1]["balls"][0]
    assert issue["category"] == "Revision"
    assert issue["action_to"] == [str(rows["erin"].id)]
    assert issue["author_id"] == str(rows["rian"].id)


def test_board_unknown_match_404(authed_client):
    resp = authed_client.get("/api/matches/nope/board")
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "MATCH_NOT_FOUND"


# ------------------------------------------------- PATCH: the field coupling


def test_patch_status_drives_review_state(authed_client, db_session):
    rows = _board(db_session)
    ball_id = str(rows["first"].id)

    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"status": "drafted"})
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "drafted"
    assert body["review_state"] == "ready for review"

    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"status": "revising"})
    assert resp.json()["review_state"] == "revision not ready"

    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"status": "creating"})
    assert resp.json()["review_state"] == "not ready"


def test_patch_review_state_drives_status(authed_client, db_session):
    rows = _board(db_session)
    ball_id = str(rows["first"].id)

    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"review_state": "approved"})
    assert resp.json()["status"] == "completed"

    resp = authed_client.patch(
        f"/api/balls/{ball_id}", json={"review_state": "revision needed"}
    )
    assert resp.json()["status"] == "revision pending"


def test_patch_no_rule_values_leave_counterpart_alone(authed_client, db_session):
    rows = _board(db_session)
    ball_id = str(rows["first"].id)

    # review side: "ready for review" has NO status rule
    resp = authed_client.patch(
        f"/api/balls/{ball_id}", json={"review_state": "ready for review"}
    )
    assert resp.json()["status"] == "open"  # untouched

    # status side: "revision pending" has NO review rule (reached via review)
    authed_client.patch(f"/api/balls/{ball_id}", json={"review_state": "revision needed"})
    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"status": "revision pending"})
    assert resp.json()["review_state"] == "revision needed"  # untouched


def test_patch_explicit_field_wins_over_automation(authed_client, db_session):
    rows = _board(db_session)
    ball_id = str(rows["first"].id)
    resp = authed_client.patch(
        f"/api/balls/{ball_id}", json={"status": "drafted", "review_state": "not ready"}
    )
    body = resp.json()
    assert body["status"] == "drafted"
    assert body["review_state"] == "not ready"  # explicit beats the coupling


def test_patch_issue_status_auto_spikes(authed_client, db_session):
    """M2: 'Ready for Review' on an issues ball IS the spike — the ball
    moves to the author, current hands are saved to rally memory, a spike
    touch is recorded. (Replaced the M1-exclusion test when the rally
    engine landed.) The issues court still has no review column."""
    rows = _board(db_session)
    ball_id = str(rows["issue"].id)

    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"status": "Ready for Review"})
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "Ready for Review"
    assert body["action_to"] == [str(rows["rian"].id)]  # spiked to the author
    assert body["prev_action_to"] == [str(rows["erin"].id)]  # rally memory
    assert body["review_state"] is None
    assert [t.kind for t in db_session.query(Touch).all()] == ["spike"]


def test_patch_other_fields_and_validation(authed_client, db_session):
    rows = _board(db_session)
    ball_id = str(rows["issue"].id)

    resp = authed_client.patch(
        f"/api/balls/{ball_id}",
        json={"title": "New title", "tags": ["footer"], "position": 5},
    )
    body = resp.json()
    assert (body["title"], body["tags"], body["position"]) == ("New title", ["footer"], 5)

    # vocab guard: a pages status is invalid on an issues court
    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"status": "drafted"})
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "INVALID_STATUS"

    # review_state does not exist on issues courts
    resp = authed_client.patch(
        f"/api/balls/{ball_id}", json={"review_state": "approved"}
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "REVIEW_STATE_NOT_ON_COURT"

    resp = authed_client.patch(f"/api/balls/{ball_id}", json={"category": "Nonsense"})
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "INVALID_CATEGORY"


def test_patch_unknown_ball_404(authed_client, db_session):
    _board(db_session)
    resp = authed_client.patch(
        "/api/balls/00000000-0000-0000-0000-000000000000", json={"title": "x"}
    )
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "BALL_NOT_FOUND"


# ----------------------------------------------------------------- POST


def test_post_creates_ball_with_court_defaults(authed_client, db_session):
    rows = _board(db_session)
    resp = authed_client.post(
        "/api/matches/testmatch/balls",
        json={"court_id": str(rows["pages"].id), "title": "Contact", "tags": ["main"]},
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["title"] == "Contact"
    assert body["status"] == "open"  # pages default
    assert body["review_state"] == "not ready"
    assert body["position"] == 2  # appended after positions 0 and 1
    assert body["author_id"] == str(rows["rian"].id)  # stamped from the session
    # M2: every rally opens with a serve — creation records the first touch.
    serves = db_session.query(Touch).all()
    assert [t.kind for t in serves] == ["serve"]
    assert serves[0].from_player == rows["rian"].id


def test_post_issue_defaults_category(authed_client, db_session):
    rows = _board(db_session)
    resp = authed_client.post(
        "/api/matches/testmatch/balls",
        json={
            "court_id": str(rows["issues"].id),
            "title": "Broken link",
            "action_to": [str(rows["erin"].id)],
        },
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["status"] == "Open"  # issues default
    assert body["category"] == "Revision"  # DEFAULT_NOTE_CATEGORY
    assert body["review_state"] is None
    assert body["action_to"] == [str(rows["erin"].id)]


def test_post_court_must_belong_to_match(authed_client, db_session):
    rows = _board(db_session)
    other = Match(slug="other", name="Other")
    db_session.add(other)
    db_session.flush()
    foreign = Court(match_id=other.id, config_key="pages", name="Pages", position=0, config={})
    db_session.add(foreign)
    db_session.commit()

    resp = authed_client.post(
        "/api/matches/testmatch/balls",
        json={"court_id": str(foreign.id), "title": "Sneaky"},
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "COURT_NOT_IN_MATCH"

    resp = authed_client.post(
        "/api/matches/ghost/balls", json={"court_id": str(rows["pages"].id), "title": "x"}
    )
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "MATCH_NOT_FOUND"


# ----------------------------------------------------------------- meta


def test_meta_serves_the_one_authority(authed_client):
    resp = authed_client.get("/api/meta")
    assert resp.status_code == 200
    meta = resp.json()

    assert meta["vocab"]["page_status"]["values"] == list(constants.PAGE_STATUSES)
    assert meta["vocab"]["review_state"]["values"] == list(constants.SIGNOFF_OPTIONS)
    assert meta["vocab"]["category"]["values"] == list(constants.NOTE_CATEGORIES)
    assert meta["vocab"]["issue_status"]["values"] == list(constants.ISSUE_STATUSES)

    # exact color language spot-checks (transcribed verbatim from the ref)
    assert meta["vocab"]["page_status"]["styles"]["revision pending"] == (
        "bg-[#ebe7cd] text-[#6d6837] border-[#d8d2a6] "
        "dark:bg-[#3a3720] dark:text-[#cbc488] dark:border-[#55502d]"
    )
    assert meta["vocab"]["review_state"]["dots"]["revision not ready"] == "bg-[#b8ad5e]"
    assert meta["vocab"]["issue_status"]["styles"]["Address Later"] == (
        "bg-slate-100 text-slate-700 border-slate-200 "
        "dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700"
    )
    assert meta["vocab"]["issue_status"]["dots"]["Address Later"] == "bg-slate-400"
    # casing rule travels with the vocab (ref §3.1)
    assert meta["vocab"]["page_status"]["capitalize"] is True
    assert meta["vocab"]["issue_status"]["capitalize"] is False

    assert meta["coupling"]["status_to_review"]["drafted"] == "ready for review"
    assert meta["coupling"]["status_to_review"]["revising"] == "revision not ready"
    assert "revision pending" not in meta["coupling"]["status_to_review"]
    assert meta["coupling"]["review_to_status"]["approved"] == "completed"
    assert meta["coupling"]["review_to_status"]["revision needed"] == "revision pending"

    assert set(meta["court_configs"]) == {"pages", "features", "issues", "warmup"}
    assert meta["court_configs"]["issues"]["ball_chip_driver"] == "action_to"
    assert meta["court_configs"]["warmup"]["ball_types"] == [
        "Request",
        "Question",
        "Requirement",
    ]


def test_meta_requires_session(client):
    resp = client.get("/api/meta")
    assert resp.status_code == 401
