"""The six-state rollup, exercised across the seed project's whole lifecycle.
Pure-function tests — no DB, no app import."""

from app.services.rollup import ProjectSnapshot, ScreenSnapshot, rollup, status_body


def snap(**kw):
    return ProjectSnapshot(**kw)


def screen(options=1, selected=False, ready=None):
    """`ready` = options carrying a mockup. Defaults to all of them; pass 0 for a
    screen shell that has been added but not uploaded into yet."""
    return ScreenSnapshot(option_count=options, selected=selected,
                          ready_count=options if ready is None else ready)


def test_empty_instance_is_not_started_never_done():
    assert rollup(snap()) == "not_started"


def test_presenting_an_empty_board_is_the_anomaly():
    """needs_attention earns its name here and nowhere else: the client has been
    handed a link to a board with nothing on it to review."""
    assert rollup(snap(sent=True)) == "needs_attention"
    # Options exist but none carry a mockup — the client sees the same nothing.
    assert rollup(snap(screens=(screen(2, ready=0),), sent=True)) == "needs_attention"


def test_options_without_mockups_are_not_ready():
    # An option the manager has created but not uploaded into is invisible to
    # the client, so it cannot make a project look reviewable.
    assert rollup(snap(screens=(screen(2, ready=0),))) == "not_started"


def test_options_but_unsent_is_in_progress():
    assert rollup(snap(screens=(screen(2),))) == "in_progress"


def test_sent_with_no_client_action_waits_on_client():
    assert rollup(snap(screens=(screen(2),), sent=True)) == "waiting_client"


def test_open_client_pin_flips_to_waiting_agency():
    s = snap(screens=(screen(2),), sent=True, open_client_pins=1)
    assert rollup(s) == "waiting_agency"


def test_agency_pins_alone_do_not_flip_the_turn():
    s = snap(screens=(screen(2),), sent=True, open_agency_pins=3)
    assert rollup(s) == "waiting_client"


def test_a_decided_screen_with_open_feedback_is_still_ours_to_answer():
    """The correction to D6. A client who picks a direction AND leaves comments
    has done exactly what the tool asks of them — that is the designed outcome
    of a round, not a conflict, and it used to raise needs_attention (the most
    alarming chip in the set) on the happy path. Open client feedback means one
    thing: we owe a reply."""
    s = snap(screens=(screen(2, selected=True),), sent=True, open_client_pins=1)
    assert rollup(s) == "waiting_agency"


def test_selection_and_approvals_complete_is_done():
    s = snap(screens=(screen(2, selected=True), screen(1, selected=True)),
             sent=True, required_steps=3, approved_required_steps=3)
    assert rollup(s) == "done"


def test_unapproved_required_steps_hold_done_back():
    s = snap(screens=(screen(2, selected=True),), sent=True,
             required_steps=2, approved_required_steps=1)
    assert rollup(s) == "waiting_client"


def test_screen_without_options_does_not_block_done():
    # A screen shell added ahead of uploads shouldn't hold the project open.
    s = snap(screens=(screen(2, selected=True), screen(0)), sent=True)
    assert rollup(s) == "done"


def test_screen_whose_options_are_all_unuploaded_does_not_block_done():
    # Same staging case, one step further along: the screen exists and has
    # options, but nothing has been uploaded into them yet.
    s = snap(screens=(screen(2, selected=True), screen(2, ready=0)), sent=True)
    assert rollup(s) == "done"


def test_resolving_the_last_client_pin_returns_the_turn():
    before = snap(screens=(screen(2),), sent=True, open_client_pins=1)
    after = snap(screens=(screen(2),), sent=True, open_client_pins=0)
    assert rollup(before) == "waiting_agency"
    assert rollup(after) == "waiting_client"


def test_status_body_shape_matches_contract():
    s = snap(screens=(screen(2, selected=True), screen(1)), sent=True,
             open_client_pins=1, open_agency_pins=2)  # 2 screens, both ready
    body = status_body("league", s, seq=7, occurred_at="2026-09-01T00:00:00Z")
    # Selection incomplete (screen 2 undecided) + an open client pin -> the
    # ball is squarely with the agency, not the ambiguous state.
    assert body["state"] == "waiting_agency"
    assert body["counts"] == {"client_open": 1, "agency_open": 2}
    assert body["seq"] == 7
    assert body["summary"] == "1 of 2 screens decided"
