"""Step-by-step tutorial guides: the spec shape, token rendering, the
normalized client payload, and the two version-movement operations
(sync_seeds publishes forward; adopt_latest_template moves an item forward
only when its history can still replay)."""

import json
from pathlib import Path

import pytest

from app.db import get_session_factory
from app.schemas import SpecError, TutorialGuide, validate_spec
from app.services import items as eng
from app.services import punchlists as pls
from app.services import workflows as wf
from tests.conftest import OWNER, as_user

SEEDS = Path(__file__).resolve().parents[1] / "app" / "seeds" / "workflows"


@pytest.fixture
def session():
    with get_session_factory()() as s:
        yield s
        s.commit()


def _spec(**step):
    base = {"id": "go", "owner": "client", "primary": {"label": "Done", "to": "$done"}}
    base.update(step)
    return {"key": "t_wf", "title": "T", "start": "go", "steps": [base]}


def test_guide_parses_and_legacy_string_still_works():
    s = validate_spec(_spec(tutorial={"title": "How", "steps": [
        {"text": "Open the thing", "note": "watch out", "image": "asset://a.png"}]}))
    g = s.steps[0].tutorial
    assert isinstance(g, TutorialGuide) and g.steps[0].note == "watch out"
    # published specs are immutable, so the old bare-string shape must live on
    assert validate_spec(_spec(tutorial="asset://x.png")).steps[0].tutorial == "asset://x.png"


def test_guide_validation_is_pointed():
    with pytest.raises(SpecError) as e:
        validate_spec(_spec(tutorial={"steps": []}))
    assert any("no steps" in m for m in e.value.errors)

    with pytest.raises(SpecError) as e:
        validate_spec(_spec(tutorial={"steps": [{"text": "  "}]}))
    assert any("empty text" in m for m in e.value.errors)

    # a token in tutorial prose is checked like any other prose
    with pytest.raises(SpecError) as e:
        validate_spec(_spec(tutorial={"steps": [{"text": "Add {{whoo}}"}]}))
    assert any("whoo" in m for m in e.value.errors)


def test_payload_is_one_shape_with_variables_rendered(session, kit):
    pl = pls.create(session, OWNER, "Tut world")
    session.commit()
    item = eng.instantiate(session, OWNER, pl.id, template_key="ga4_admin_access",
                           variables={"who_needs_access": ["a@x.com", "b@y.com"]})
    session.commit()
    tut = eng.render_item(session, item, for_team=True)["step"]["tutorial"]
    assert tut["steps"] and tut["title"]
    assert any("a@x.com and b@y.com" in s["text"] for s in tut["steps"])
    assert all({"text", "note", "image"} <= set(s) for s in tut["steps"])


def test_every_shipped_guide_is_usable():
    """A guide with a step whose text is a bare click-path and nothing else is
    fine; one with no steps, or an unrenderable token, would ship broken."""
    guides = 0
    for f in sorted(SEEDS.glob("*.json")):
        spec = validate_spec(json.loads(f.read_text()))
        for st in spec.steps:
            if isinstance(st.tutorial, TutorialGuide):
                guides += 1
                assert st.tutorial.steps, f"{f.name}:{st.id}"
                assert all(s.text.strip() for s in st.tutorial.steps)
                assert st.owner == "client", (
                    f"{f.name}:{st.id} — guides are for the person DOING the task")
    assert guides >= 10


def _wf(version_marker, start="a", a_id="a", tutorial=None):
    step_a = {"id": a_id, "owner": "client", "instruction": version_marker,
              "primary": {"label": "Next", "to": "b"}}
    if tutorial:
        step_a["tutorial"] = tutorial
    return {"key": "t_adopt", "title": "Adopt me", "start": start, "steps": [
        step_a,
        {"id": "b", "owner": "team", "primary": {"label": "Done", "to": "$done"}},
    ]}


def test_adopt_latest_moves_forward_when_the_change_is_additive(session, kit):
    pl = pls.create(session, OWNER, "Adopt world")
    wf.publish(session, OWNER, _wf("v1 text"))
    session.commit()
    item = eng.instantiate(session, OWNER, pl.id, template_key="t_adopt")
    session.commit()
    assert eng.render_item(session, item, for_team=True)["step"]["tutorial"] is None

    # a new version that only ADDS (a tutorial, better prose) is safe to adopt
    wf.publish(session, OWNER, _wf("v2 text", tutorial={
        "title": "How", "steps": [{"text": "Open the thing"}]}))
    session.commit()
    r = eng.adopt_latest_template(session, item, OWNER)
    session.commit()
    assert r["changed"] and r["from_version"] == 1 and r["version"] == 2
    rendered = eng.render_item(session, item, for_team=True)["step"]
    assert rendered["tutorial"]["steps"][0]["text"] == "Open the thing"
    assert rendered["instruction"] == "v2 text"

    # already on the newest: a no-op result, not an error
    assert eng.adopt_latest_template(session, item, OWNER)["changed"] is False


def test_adopt_refuses_a_version_that_dropped_a_visited_step(session, kit):
    pl = pls.create(session, OWNER, "Adopt refuse")
    wf.publish(session, OWNER, _wf("v1"))
    session.commit()
    item = eng.instantiate(session, OWNER, pl.id, template_key="t_adopt")
    session.commit()

    # v2 renames the step the item is sitting on — its history could not replay
    wf.publish(session, OWNER, _wf("v2", start="z", a_id="z"))
    session.commit()
    with pytest.raises(eng.ItemError) as e:
        eng.adopt_latest_template(session, item, OWNER)
    assert e.value.code == "INCOMPATIBLE_VERSION"
    assert "a" in str(e.value)
    # and the item is untouched — still running v1
    assert wf.get_template(session, item.template_id).version == 1


def test_adopt_all_reports_what_it_could_not_move(client, kit):
    as_user(client, OWNER)
    pid = client.post("/api/punchlists", json={"title": "Mixed"}).json()["id"]
    client.post(f"/api/punchlists/{pid}/items",
                json={"template_key": "ga4_admin_access",
                      "variables": {"who_needs_access": ["a@x.com"]}})
    # a one-off inline item can never adopt — it must be REPORTED, not skipped
    client.post(f"/api/punchlists/{pid}/items", json={"spec": {
        "key": "oneoff", "title": "One off", "start": "go", "steps": [
            {"id": "go", "owner": "client", "primary": {"label": "Ok", "to": "$done"}}]}})
    r = client.post(f"/api/punchlists/{pid}/adopt-latest-templates")
    assert r.status_code == 200, r.text
    body = r.json()
    assert len(body["held"]) == 1 and body["held"][0]["error_code"] == "BAD_INPUT"


def test_sync_seeds_is_idempotent_and_only_publishes_changes(session):
    before = {t["key"]: t["version"] for t in wf.library(session)}
    assert wf.sync_seeds(session, OWNER) == []      # seeds already match on a fresh DB
    session.commit()
    assert {t["key"]: t["version"] for t in wf.library(session)} == before


def test_adopt_endpoints_are_team_gated(client, kit):
    as_user(client, OWNER)
    pid = client.post("/api/punchlists", json={"title": "Gate"}).json()["id"]
    iid = client.post(f"/api/punchlists/{pid}/items",
                      json={"template_key": "ga4_admin_access",
                            "variables": {"who_needs_access": ["a@x.com"]}}).json()["id"]
    kit.member("cleo", "member")
    kit.grant("cleo", pid, "member")
    as_user(client, "cleo")
    assert client.post(f"/api/items/{iid}/adopt-latest-template").status_code == 403
    assert client.post(f"/api/punchlists/{pid}/adopt-latest-templates").status_code == 403
    assert client.post("/api/workflows/sync-seeds").status_code == 403
