"""The item engine (plan §2, §6, §7): turn-taking, per-target completion,
alternatives, flags, jump-backs — and the replay oracle proving that
item.status / current_step_id are honest caches of the event log.
"""

import pytest

from app import accounts
from app import bw_accounts as bwa
from app.db import get_session_factory
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()


@pytest.fixture
def punchlist(session):
    return pls.create(session, OWNER, "Brentwood onboarding")


def _replay_check(session, item):
    """The oracle: cached state must equal a pure replay of the events."""
    spec = eng.spec_of(session, item)
    step, status = eng.replay_status(spec, eng.events_of(session, item))
    assert (item.current_step_id, item.status) == (step, status)


def _ga4(session, punchlist, who=("rian@rian.ca", "heather@bowden.cc")):
    return eng.instantiate(session, OWNER, punchlist.id,
                           template_key="ga4_admin_access",
                           variables={"who_needs_access": list(who)})


# ------------------------------------------------------------- instantiation

def test_missing_required_variables_refused(session, punchlist):
    with pytest.raises(eng.ItemError) as e:
        eng.instantiate(session, OWNER, punchlist.id, template_key="ga4_admin_access")
    assert e.value.code == "MISSING_VARIABLES"


def test_unknown_template_refused(session, punchlist):
    with pytest.raises(eng.ItemError) as e:
        eng.instantiate(session, OWNER, punchlist.id, template_key="nope")
    assert e.value.code == "NO_SUCH_TEMPLATE"


def test_inline_spec_one_off(session, punchlist):
    """The manager quick-add: a degenerate one-step workflow IS a checkbox."""
    item = eng.instantiate(session, OWNER, punchlist.id, spec_inline={
        "key": "oneoff_shopify", "title": "Shopify access", "start": "grant",
        "steps": [{"id": "grant", "owner": "client",
                   "headline": "Add us as a Shopify staff account",
                   "primary": {"label": "Added", "to": "$done"}}]})
    assert item.status == "waiting_on_client"
    eng.act(session, item, actor="clienta", actor_kind="client", action="primary")
    assert item.status == "done" and item.current_step_id is None
    _replay_check(session, item)


# ------------------------------------------------------------- per-target

def test_per_target_completion(session, punchlist):
    item = _ga4(session, punchlist)
    assert item.status == "waiting_on_client"
    r = eng.render_item(session, item, for_team=False)
    assert r["label"] == "Grant GA4 admin access to rian@rian.ca and heather@bowden.cc"
    # the label survives a step change untouched (goal-label rule)
    eng.act(session, item, actor="c", actor_kind="client", action="no_admin_option")
    assert eng.render_item(session, item, for_team=False)["label"] == r["label"]
    eng.act(session, item, actor=OWNER, actor_kind="team", action="set_step", to="grant_admins")

    eng.act(session, item, actor="clienta", actor_kind="client",
            action="primary", target="rian@rian.ca")
    assert item.current_step_id == "grant_admins", "one of two targets: still on the step"
    assert item.status == "waiting_on_client"

    # same target twice is refused
    with pytest.raises(eng.ItemError) as e:
        eng.act(session, item, actor="clienta", actor_kind="client",
                action="primary", target="rian@rian.ca")
    assert e.value.code == "TARGET_DONE"

    eng.act(session, item, actor="clienta", actor_kind="client",
            action="primary", target="heather@bowden.cc")
    assert item.current_step_id == "team_verify", "all targets done: advances"
    assert item.status == "waiting_on_team"
    _replay_check(session, item)


def test_jump_back_resets_targets(session, punchlist):
    """Re-entering a per-target step via a jump is a fresh ask (plan/engine
    docstring): otherwise 'all checked but someone is missing' deadlocks."""
    item = _ga4(session, punchlist)
    for t in ("rian@rian.ca", "heather@bowden.cc"):
        eng.act(session, item, actor="c", actor_kind="client", action="primary", target=t)
    assert item.current_step_id == "team_verify"

    eng.act(session, item, actor=OWNER, actor_kind="team", action="missing_account")
    assert item.current_step_id == "grant_admins"
    spec = eng.spec_of(session, item)
    state = eng.targets_state(spec, item, eng.events_of(session, item))
    assert all(not t["done"] for t in state), "fresh entry: no target pre-checked"
    _replay_check(session, item)


# ------------------------------------------------------------- alternatives

def test_conditional_unfold_via_jump(session, punchlist):
    """'I don't have an option to add admins' — the conditional-unfolding case."""
    item = _ga4(session, punchlist)
    eng.act(session, item, actor="c", actor_kind="client", action="no_admin_option")
    assert item.current_step_id == "check_property_access"
    eng.act(session, item, actor="c", actor_kind="client", action="primary")
    assert item.current_step_id == "grant_property_admins"
    assert item.status == "waiting_on_client"
    _replay_check(session, item)


def test_ack_flag_stays_open_without_alarm(session, punchlist):
    item = _ga4(session, punchlist)
    eng.act(session, item, actor="c", actor_kind="client", action="later")
    assert item.status == "waiting_on_client", "ack tone: no alarm"
    assert item.flag == "later"
    _replay_check(session, item)


def test_attention_flag_requires_message_and_raises_alarm(session, punchlist):
    item = _ga4(session, punchlist)
    with pytest.raises(eng.ItemError) as e:
        eng.act(session, item, actor="c", actor_kind="client", action="trouble")
    assert e.value.code == "MESSAGE_REQUIRED"

    eng.act(session, item, actor="c", actor_kind="client", action="trouble",
            message="The admin page says I need to contact my administrator")
    assert item.status == "needs_attention"
    _replay_check(session, item)


def test_other_is_implicit_attention(session, punchlist):
    item = _ga4(session, punchlist)
    eng.act(session, item, actor="c", actor_kind="client", action="other",
            message="Our IT company handles analytics, passing this to them")
    assert item.status == "needs_attention" and item.flag == "other"
    _replay_check(session, item)


def test_resolve_flag_is_team_only_with_reply(session, punchlist):
    item = _ga4(session, punchlist)
    eng.act(session, item, actor="c", actor_kind="client", action="trouble", message="stuck")

    with pytest.raises(eng.ItemError):
        eng.act(session, item, actor="c", actor_kind="client",
                action="resolve_flag", message="nope")
    with pytest.raises(eng.ItemError) as e:
        eng.act(session, item, actor=OWNER, actor_kind="team", action="resolve_flag")
    assert e.value.code == "MESSAGE_REQUIRED"

    eng.act(session, item, actor=OWNER, actor_kind="team", action="resolve_flag",
            message="No problem — ask your admin to open Account access, or send us their email")
    assert item.status == "waiting_on_client" and item.flag is None
    rendered = eng.render_item(session, item, for_team=False)
    texts = [m["text"] for m in rendered["messages"]]
    assert any("stuck" in t for t in texts) and any("No problem" in t for t in texts)
    _replay_check(session, item)


# ------------------------------------------------------------- full walk

def test_google_ads_full_walk_with_fields(session, punchlist):
    item = eng.instantiate(session, OWNER, punchlist.id,
                           template_key="google_ads_access",
                           variables={"who_needs_access": ["rian@rian.ca"]})
    # bad field format refused with a helpful message
    with pytest.raises(eng.ItemError) as e:
        eng.act(session, item, actor="c", actor_kind="client", action="primary",
                fields={"customer_id": "12345"})
    assert e.value.code == "FIELD_INVALID"
    # ...but the dashes are optional: people paste it both ways, and refusing
    # a correct ID over punctuation is the kind of thing that stalls an item
    eng._validate_fields(
        next(st for st in eng.spec_of(session, item).steps if st.id == "submit_customer_id"),
        {"customer_id": "1234567890"})

    eng.act(session, item, actor="c", actor_kind="client", action="primary",
            fields={"customer_id": "123-456-7890"})
    assert item.current_step_id == "team_request" and item.status == "waiting_on_team"

    # captured field interpolates into the TEAM step's instruction
    rendered = eng.render_item(session, item, for_team=True)
    assert "123-456-7890" in rendered["step"]["instruction"]
    # ...while the LABEL keeps the goal, unchanged by the step
    assert rendered["label"] == "Give us access to Google Ads"

    # client cannot act on a team step
    with pytest.raises(eng.ItemError) as e:
        eng.act(session, item, actor="c", actor_kind="client", action="primary")
    assert e.value.code == "NOT_YOUR_STEP"

    eng.act(session, item, actor=OWNER, actor_kind="team", action="primary")
    assert item.current_step_id == "client_accept"
    eng.act(session, item, actor="c", actor_kind="client", action="primary")
    eng.act(session, item, actor=OWNER, actor_kind="team", action="primary")
    assert item.status == "done"

    # done item refuses further acts
    with pytest.raises(eng.ItemError) as e:
        eng.act(session, item, actor="c", actor_kind="client", action="primary")
    assert e.value.code == "ITEM_DONE"
    _replay_check(session, item)


def test_client_perception_mapping(session, punchlist):
    """Plan §2: a team step reads as waiting_on_us (their work LOOKS done)."""
    item = eng.instantiate(session, OWNER, punchlist.id,
                           template_key="google_ads_access",
                           variables={"who_needs_access": ["rian@rian.ca"]})
    assert eng.render_item(session, item, for_team=False)["client_state"] == "todo"
    eng.act(session, item, actor="c", actor_kind="client", action="primary",
            fields={"customer_id": "123-456-7890"})
    assert eng.render_item(session, item, for_team=False)["client_state"] == "waiting_on_us"


def test_set_step_override_is_team_only(session, punchlist):
    item = _ga4(session, punchlist)
    with pytest.raises(eng.ItemError):
        eng.act(session, item, actor="c", actor_kind="client",
                action="set_step", to="team_verify")
    eng.act(session, item, actor=OWNER, actor_kind="team",
            action="set_step", to="grant_property_admins", message="client emailed: no account access")
    assert item.current_step_id == "grant_property_admins"
    _replay_check(session, item)


# ------------------------------------------------------------- library

def test_publish_versions_never_mutate(session):
    raw = {"key": "vtest", "title": "V1", "start": "a",
           "steps": [{"id": "a", "owner": "client", "headline": "x",
                      "primary": {"label": "Done", "to": "$done"}}]}
    r1 = wf.publish(session, OWNER, raw)
    raw2 = dict(raw, title="V2")
    r2 = wf.publish(session, OWNER, raw2)
    assert (r1.version, r2.version) == (1, 2)
    assert r1.title == "V1", "publishing v2 never touched v1"
