"""Control types: how a step's answer is COLLECTED, as distinct from what
counts as done (the row's checkbox). Covers the spec guards and the one
genuinely new engine concept, a status list."""

import pytest

from app.db import get_session_factory
from app.schemas import SpecError, 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


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


def _spec(**a):
    """The downstream step only quotes {{fields.a.state}} when step a actually
    has states — a token is only valid against the shape that produces it."""
    step = {"id": "a", "owner": "client", "primary": {"label": "Done", "to": "b"}}
    step.update(a)
    after = {"id": "b", "owner": "team", "primary": {"label": "Noted", "to": "$done"}}
    if a.get("states"):
        after["instruction"] = "Status was {{fields.a.state}}."
    return {"key": "t_ctl", "title": "T", "start": "a", "steps": [step, after]}


FIELD = [{"key": "v", "type": "text", "label": "V"}]


def test_a_collecting_control_takes_exactly_one_field():
    validate_spec(_spec(control="input", fields=FIELD))
    for bad, needle in [
        ({"control": "input"}, "exactly"),
        ({"control": "input", "fields": FIELD + [{"key": "w", "type": "text", "label": "W"}]},
         "exactly"),
        ({"control": "upload"}, "exactly"),
    ]:
        with pytest.raises(SpecError) as e:
            validate_spec(_spec(**bad))
        assert any(needle in m for m in e.value.errors), e.value.errors


def test_link_sends_somewhere_and_collects_nothing():
    validate_spec(_spec(control="link", link={"label": "Open", "url": "https://x.com/f"}))
    with pytest.raises(SpecError) as e:
        validate_spec(_spec(control="link"))
    assert any("needs a" in m for m in e.value.errors)

    with pytest.raises(SpecError) as e:
        validate_spec(_spec(control="link", link={"label": "O", "url": "ftp://x"}))
    assert any("http(s)" in m for m in e.value.errors)

    with pytest.raises(SpecError) as e:
        validate_spec(_spec(control="link", link={"label": "O", "url": "https://x.com"},
                            fields=FIELD))
    assert any("collects nothing" in m for m in e.value.errors)

    # a link without the control that owns it
    with pytest.raises(SpecError) as e:
        validate_spec(_spec(link={"label": "O", "url": "https://x.com"}))
    assert any("belongs to control" in m for m in e.value.errors)


def test_status_list_shape():
    validate_spec(_spec(states=["One", "Two"]))
    for bad, needle in [
        ({"states": ["Only"]}, "at least two"),
        ({"states": ["Same", "Same"]}, "duplicate state"),
        ({"states": ["A", "B"], "fields": FIELD}, "can't also collect"),
        ({"states": ["A", "B"], "control": "input"}, "IS the control"),
    ]:
        with pytest.raises(SpecError) as e:
            validate_spec(_spec(**bad))
        assert any(needle in m for m in e.value.errors), e.value.errors


def test_status_advances_only_on_the_LAST_state(session, kit):
    pl = pls.create(session, OWNER, "Status world")
    wf.publish(session, OWNER, _spec(states=["Waiting", "Received", "Paid"]))
    session.commit()
    item = eng.instantiate(session, OWNER, pl.id, template_key="t_ctl")
    session.commit()

    r = eng.render_item(session, item, for_team=True)["step"]
    assert r["states"] == ["Waiting", "Received", "Paid"] and r["state_index"] == 0

    # an intermediate state records and STAYS — "where is this up to" matters
    eng.act(session, item, actor="cleo", actor_kind="client",
            action="state", fields={"state": "Received"})
    session.commit()
    r = eng.render_item(session, item, for_team=True)["step"]
    assert r["id"] == "a" and r["state_index"] == 1

    # you can move it back
    eng.act(session, item, actor="cleo", actor_kind="client",
            action="state", fields={"state": "Waiting"})
    session.commit()
    assert eng.render_item(session, item, for_team=True)["step"]["state_index"] == 0

    # the last state is what finishes the step, and it reads downstream
    eng.act(session, item, actor="cleo", actor_kind="client",
            action="state", fields={"state": "Paid"})
    session.commit()
    r = eng.render_item(session, item, for_team=True)["step"]
    assert r["id"] == "b" and r["instruction"] == "Status was Paid."


def test_unknown_state_is_refused(session, kit):
    pl = pls.create(session, OWNER, "Refuse states")
    wf.publish(session, OWNER, _spec(states=["A", "B"]))
    session.commit()
    item = eng.instantiate(session, OWNER, pl.id, template_key="t_ctl")
    session.commit()
    with pytest.raises(eng.ItemError) as e:
        eng.act(session, item, actor="cleo", actor_kind="client",
                action="state", fields={"state": "Nope"})
    assert e.value.code == "NO_SUCH_ACTION"


def test_every_shipped_workflow_declares_a_usable_control():
    """A control exists to COLLECT or to SEND somewhere. If a step needs
    neither, it must not declare one — the checkbox takes claims."""
    import json
    from pathlib import Path
    seeds = Path(__file__).resolve().parents[1] / "app" / "seeds" / "workflows"
    seen = set()
    for f in sorted(seeds.glob("*.json")):
        spec = validate_spec(json.loads(f.read_text()))
        for st in spec.steps:
            seen.add(st.control)
            if st.control in ("input", "upload"):
                assert len(st.fields) == 1, f"{f.name}:{st.id}"
            if st.control == "link":
                assert st.link and not st.fields, f"{f.name}:{st.id}"
    assert {"input", "upload", "link"} <= seen, f"only saw {seen}"
