from controllers.types import CONTROLLERS, Docs, Messages, Todos
from resources.base import AGENT, USER
from tests.conftest import fresh, refused


def test_a_dump_is_read_and_filed_item_by_item_and_closes_when_every_item_is_settled(tmp_path):
    record = fresh()
    dumps = CONTROLLERS["dump"](record, actor=USER)
    dropped = tmp_path / "notes.md"
    dropped.write_text("# meeting notes")
    dump = dumps.create("Tuesday's meeting", brief="the transcript of the meeting")
    dumps.attach(dump.n, str(dropped))
    agent = CONTROLLERS["dump"](record, actor=AGENT)
    assert agent.items(dump.n) == ["text: not read yet", "notes.md: not read yet"], "the pasted text and every file are items"
    assert dump.title == f"Dump {dump.n}", "a dump is titled by its number until the agent names it"

    agent.note(dump.n, "text", "a transcript of Tuesday's planning meeting")
    named = agent.name(dump.n, "Q3 planning meeting")
    doc, task = Docs(record, actor=AGENT).create("Tuesday's meeting"), Todos(record, actor=AGENT).create("send the recap")
    agent.filed(dump.n, "text", "summarised into a doc, one follow-up filed", f"{doc.ref}, to-do {task.n}")
    assert agent.load(dump.n).data["items"]["text"]["refs"] == [doc.ref, task.ref], "a row named the way a person writes it is kept as its ref"
    assert (agent.items(dump.n)[0], set(agent.load(dump.n).refs) >= {doc.ref, task.ref}) == \
        ("text: filed - summarised into a doc, one follow-up filed", True), "filed records what was done and links what it made"
    collection = CONTROLLERS["collection"](record, actor=USER).load(named.n)
    assert (collection.title, {doc.ref, task.ref} <= set(collection.refs)) == ("Q3 planning meeting", True), \
        "the agent names the collection, and what is filed later still goes into it"
    assert agent.load(dump.n).title == "Q3 planning meeting", "naming the collection names the dump too"

    assert "has no item 'other.md'" in refused(lambda: agent.note(dump.n, "other.md", "x")), "an item the dump does not have is refused"
    assert "say why" in refused(lambda: agent.failed(dump.n, "notes.md", " ")), "a failure needs words"
    agent.failed(dump.n, "notes.md", "the file is empty")
    closed = agent.load(dump.n)
    assert (bool(closed.completed), closed.outcome) == (True, "1 filed, 1 failed"), "the last settled item closes the dump"
    assert agent.items(dump.n)[1] == "notes.md: failed - the file is empty", "an item that failed says why in the list"
    assert refused(lambda: agent.log(dump.n, "Adding files")) == f"dump {dump.n} is closed", "a closed dump takes no more of the agent's progress lines"
    assert refused(lambda: dumps.attach(dump.n, str(dropped))) == f"dump {dump.n} is already filed: start a new dump for more", \
        "more can be dropped only while a dump is still filing"


def test_the_agent_is_told_when_a_dump_arrives_and_when_more_is_dropped_on_it(tmp_path):
    from tests.kit import nudges, report
    record = fresh()
    report(record, "working", "PreToolUse")
    dumps = CONTROLLERS["dump"](record, actor=USER)
    dump = dumps.create("A pile", brief="some pasted text")
    arrived = [n for n in nudges(record) if n.startswith(f"dump {dump.n}")]
    assert arrived == [f"dump {dump.n} has 1 item to file - journal dump items {dump.n}"], "a new dump is named with what waits"
    more = tmp_path / "later.png"
    more.write_bytes(b"png")
    dumps.attach(dump.n, str(more))
    assert f"dump {dump.n} has 2 items to file - journal dump items {dump.n}" in nudges(record), "a file dropped on it later is named again"
    CONTROLLERS["dump"](record, actor=AGENT).create("the agent's own", brief="notes")
    assert not [n for n in nudges(record) if "the agent's own" in n], "a dump the agent made is not announced to it"


def test_what_a_dump_files_is_in_the_journal_at_once_and_removing_it_takes_out_only_what_it_made():
    record = fresh()
    earlier = Docs(record, actor=AGENT).create("An older doc")
    dump = CONTROLLERS["dump"](record, actor=USER).create("Launch notes", brief="notes")
    collections, docs, todos = CONTROLLERS["collection"](record, actor=USER), Docs(record, actor=USER), Todos(record, actor=USER)
    agent = CONTROLLERS["dump"](record, actor=AGENT)
    doc, task = Docs(record, actor=AGENT).create("The launch"), Todos(record, actor=AGENT).create("book the room")
    agent.filed(dump.n, "text", "a doc and a to-do, and the older doc extended", f"{doc.ref}, {task.ref}, {earlier.ref}", added=task.ref)
    made = collections.load(agent._collection(agent.load(dump.n)))
    assert collections.members(made.n) == ["doc 2  The launch", "todo 1  book the room", "doc 1  An older doc"], \
        "the dump's collection holds everything it filed"
    assert made.source == dump.ref, "the collection names the dump it came from, which is not one of its members"
    assert ([c.n for c in collections.all()], sorted(d.n for d in docs.all()), [t.n for t in todos.all()]) == ([made.n], [earlier.n, doc.n], [task.n]), \
        "what it files is listed at once, with nothing to confirm"
    assert agent.load(dump.n).data["items"]["text"]["added"] == [task.ref], "the agent marks what it wrote without being asked"
    assert refused(lambda: agent.remove(dump.n)) == "only the user removes what a dump filed", "the agent cannot remove it"
    todos.force_delete(task.n)
    collections.delete(made.n, why="tidied by hand")
    removed = CONTROLLERS["dump"](record, actor=USER).remove(dump.n)
    assert ([c.n for c in collections.all()], [d.n for d in docs.all()], [t.n for t in todos.all()]) == ([], [earlier.n], []), \
        "removing takes out the collection and all it made, and keeps a row it only extended"
    assert (bool(removed.completed), removed.data["removed_refs"]) == (True, [doc.ref]), "the dump closes and says what it took out, passing over what is already gone"


def test_one_dump_is_worked_at_a_time_its_log_is_kept_and_filing_asks_for_the_next_step():
    from tests.kit import nudges, report
    record = fresh()
    report(record, "working", "PreToolUse")
    dump = CONTROLLERS["dump"](record, actor=USER).create("One note", brief="a note")
    later = CONTROLLERS["dump"](record, actor=USER).create("Another", brief="more")
    assert not [n for n in nudges(record) if n.startswith(f"dump {later.n},")], "a dump dropped while one is open waits"
    agent = CONTROLLERS["dump"](record, actor=AGENT)
    agent.log(dump.n, "reading the note")
    agent.ask(dump.n, "is this about the launch or the audit")
    assert refused(lambda: agent.answer(dump.n, "the launch")) == "only the user answers a question on a dump", "the agent cannot answer itself"
    CONTROLLERS["dump"](record, actor=USER).answer(dump.n, "the launch")
    assert (nudges(record)[-1], agent.load(dump.n).data["question"]) == \
        (f"the user answered your question on dump {dump.n} - the launch", {}), "the answer reaches the agent and the question clears"
    assert [e["text"] for e in agent.load(dump.n).data["log"]] == ["reading the note"], "the log keeps what the agent said it is doing"
    assert refused(lambda: agent.log(dump.n, " ")) == "say what you are doing", "a log entry needs words"
    agent.failed(dump.n, "text", "nothing in it to keep")
    assert f"dump {dump.n} is filed (0 filed, 1 failed) - sum it up for the user" in nudges(record), "closing the dump asks for a summary"
    assert nudges(record)[-1] == f"dump {later.n} has 1 item to file - journal dump items {later.n}", "and hands over the next one"
    CONTROLLERS["dump"](record, actor=USER).reopen(dump.n, "more to add")
    assert agent._in_hand().n == later.n, "a reopened dump joins the back of the queue, behind the one being filed"
    CONTROLLERS["dump"](record, actor=USER).stop(later.n)
    assert (agent.load(later.n).outcome, [n for n in nudges(record) if n.startswith(f"dump {later.n} is filed")]) == \
        ("stopped, 0 filed, 1 left out", []), "a stopped dump says what was left out and asks for no next step"


def test_a_message_declared_a_transcript_becomes_a_dump(tmp_path):
    from controllers.types import Messages
    record = fresh()
    standup = Messages(record, actor=USER).create("Standup", brief="Alice: ship it. Bob: tests first.")
    notes = tmp_path / "audio-notes.txt"
    notes.write_text("notes")
    Messages(record, actor=USER).attach(standup.n, str(notes))
    Messages(record, actor=AGENT).declare(standup.n, "transcript")
    dumps = CONTROLLERS["dump"](record, actor=AGENT)
    made = [d for d in dumps.all() if standup.ref in d.refs]
    assert (len(made), made[0].brief, dumps.items(made[0].n)) == (1, standup.brief, ["text: not read yet", "audio-notes.txt: not read yet"]), \
        "its text and its files become the dump's items"
    assert made[0].ref in Messages(record, actor=USER).load(standup.n).refs, "the message links the dump"


def test_an_idle_agent_is_told_to_carry_on_filing_even_with_auto_off():
    from tests.kit import idle, nudges, tick
    record = fresh()
    dump = CONTROLLERS["dump"](record, actor=USER).create("Left half done", brief="notes")
    idle(record)
    tick(record)
    tick(record)
    assert [n for n in nudges(record) if "carry on" in n] == [f"dump {dump.n} still has 1 item to file - carry on filing it"], \
        "an agent resting with a dump unfinished is told once to carry on, auto mode or not"


def test_a_filed_dump_is_summed_up_with_suggestions_the_user_takes_or_leaves():
    from tests.kit import nudges, report
    record = fresh()
    report(record, "working", "PreToolUse")
    dump = CONTROLLERS["dump"](record, actor=USER).create("Plan notes", brief="notes")
    agent, user = CONTROLLERS["dump"](record, actor=AGENT), CONTROLLERS["dump"](record, actor=USER)
    agent.filed(dump.n, "text", "a to-do", f"{Todos(record, actor=AGENT).create('book the room').ref}")
    assert "not a command" in refused(lambda: agent.offer(dump.n, '[{"label": "Fly", "type": "todo", "action": "fly"}]')), "an unknown action is refused"
    assert refused(lambda: agent.offer(dump.n, "[]")).startswith("sum up what you filed"), "an offer carries a summary or a step"
    agent.offer(dump.n, '[{"ask": "Book the room today?", "label": "Book it"}, {"label": "Make a plan for the autoscaler and the deployment framework"}]',
                summary="Filed one to-do.")
    offered = agent.load(dump.n).data
    assert (offered["summary"], offered["options"][0]["ask"], offered["options"][1]["label"]) == \
        ("Filed one to-do.", "Book the room today?", "Make a plan for the autoscaler and the deployment framework"), \
        "the summary and each suggestion are kept whole"
    assert refused(lambda: agent.choose(dump.n, 0)) == "only the user chooses what a dump does next", "the agent cannot choose"
    user.choose(dump.n, 0)
    assert nudges(record)[-1] == f"the user chose Book it for dump {dump.n}", "taking a suggestion tells the agent"
    assert "already taken" in refused(lambda: user.choose(dump.n, 0)), "a suggestion is taken once"
    user.decline(dump.n, 1)
    assert (list(agent.load(dump.n).data["taken"]), agent.load(dump.n).data["declined"]) == (["0"], [1]), "what was taken and left is kept"
    user.direct(dump.n, "Where did the room booking go?")
    asked = [m for m in Messages(record, actor=USER).rows.standing() if dump.ref in m.refs]
    assert [(m.brief, m.data["window"]) for m in asked] == [("Where did the room booking go?", dump.ref)], \
        "the user's own words reach the agent as a message written in the dump's window, which the main chat leaves out"
    Messages(record, actor=AGENT).read(asked[0].n)
    assert f"journal dump say {dump.n}" in refused(lambda: Messages(record, actor=AGENT).reply(asked[0].n, "Under Meetings.")), \
        "a question asked in the dump is answered in the dump, never in the main chat"
    agent.say(dump.n, "It is under Meetings, as Room booking for Thursday.")
    assert agent.load(dump.n).data["log"][-1]["answer"], "an answer in the dump lands in its chat"
    assert not [c for c in CONTROLLERS["agent"](record).primary().data.get("cards") or [] if "dump" in c["label"]], "and nothing about it reaches the main chat"
    agent.log(dump.n, "Booked the room", detail="Room 4, Thursday")
    assert agent.load(dump.n).data["log"][-1]["text"] == "Booked the room", "the agent logs what it does after the summary"
    agent.offer(dump.n, '[{"ask": "a step with no label"}, {"label": "Add a to-do", "type": "todo", "action": "create", "body": {"title": "Confirm the room"}}]')
    assert [step["label"] for step in agent.load(dump.n).data["options"]] == ["Add a to-do"], "a step offered without a label is left out"
    user.choose(dump.n, 0)
    assert (user.choose(dump.n, -1).data["chosen"]["label"], "-1" in user.load(dump.n).data["taken"]) == ("You decide", False), \
        "You decide hands the choice back without taking a step"
    assert [row.title for row in Todos(record, actor=AGENT).rows.standing()].count("Confirm the room") == 1, "a suggestion that names a command runs it when the user takes it"
    assert "has no option 7" in refused(lambda: user.choose(dump.n, 7)) and "has no option 7" in refused(lambda: user.decline(dump.n, 7)), \
        "only an offered step is taken or declined"
    assert user.dismiss(dump.n).data["dismissed"], "a filed dump can be dismissed from the list"
    user.remove(user.create("Not yet closed").n)
    assert any(row.data.get("removed") for row in user.rows.every()), "removing a dump that is still open stops it first"


def test_pasted_text_splits_into_parts_and_a_question_carries_guesses():
    record = fresh()
    dump = CONTROLLERS["dump"](record, actor=USER).create("Standup", brief="summary, transcript and a link")
    agent = CONTROLLERS["dump"](record, actor=AGENT)
    agent.split(dump.n, "Summary, Transcript, Link")
    assert agent.items(dump.n) == ["Summary: not read yet", "Transcript: not read yet", "Link: not read yet"], "each part is an item of its own"
    agent.ask(dump.n, "Which doc is the link for?", guesses="The standup | Neither")
    assert refused(lambda: agent.log(dump.n, "I am writing the rollout plan that the standup asked for")).startswith("a status is a short title"), \
        "a status is a short title"
    agent.log(dump.n, "Writing the rollout plan", making="plan, Autoscaler rollout", detail="The standup set a goal")
    written = agent.load(dump.n)
    assert (written.data["question"]["guesses"], written.data["log"][-1]["making"]) == (["The standup", "Neither"], "plan, Autoscaler rollout"), \
        "a question carries the agent's guesses and a log line names what it is about to make"
    a_dump_refuses_what_it_cannot_do(record, dump)


def a_dump_refuses_what_it_cannot_do(record, dump):
    user = CONTROLLERS["dump"](record, actor=USER)
    agent = CONTROLLERS["dump"](record, actor=AGENT)
    assert "not valid" in refused(lambda: user.offer(dump.n, "{broken")) or "JSON list" in refused(lambda: user.offer(dump.n, "{broken")), "an offer that is not JSON is refused with its shape"
    assert "no pasted text" in refused(lambda: user.split(user.create("Empty").n, "A, B")), "a dump with no pasted text has nothing to split"
    assert "name the parts" in refused(lambda: user.split(dump.n, " , ")), "a split names at least one part"
    assert [bool(refused(lambda: user.say(dump.n, text))) for text in ("", "x" * 2000)] == [True, True], "an answer in the dump is neither empty nor long"
    assert "say what you need to know" in refused(lambda: agent.ask(dump.n, " ")), "a question asks something"
    assert "say the answer" in refused(lambda: user.answer(dump.n, " ")), "an answer says something"
    assert "no question waiting" in refused(lambda: user.answer(user.create("Quiet").n, "yes")), "an answer needs a question to answer"
    assert "say what was done" in refused(lambda: agent.filed(dump.n, "Summary", " ")), "a filed item says what was done with it"
    assert "also list in refs" in refused(lambda: agent.filed(dump.n, "Summary", "Filed", refs="doc:1", added="doc:2")), "an item names as added only rows it also lists"
    assert "names no item" in refused(lambda: agent.filed(dump.n, "Summary", "Filed", refs="commit abc")), "a filed item links rows, never free words"
    assert "say why" in refused(lambda: agent.failed(dump.n, "Summary", " ")), "a failed item says why"
    assert "only the user stops" in refused(lambda: agent.stop(dump.n)), "the agent never stops a dump"
    loose = user.create("Loose")
    user.unlink(loose.n, loose.refs[0])
    assert "no collection" in refused(lambda: user.name(loose.n, "Named")) and "say the name" in refused(lambda: user.name(dump.n, " ")), \
        "a dump is named only once it has a collection, and with a name"
    assert "say what should happen" in refused(lambda: user.direct(dump.n, " ")), "directing a dump says what should happen next"
    finished = user.create("Done")
    user.stop(finished.n)
    assert "already closed" in refused(lambda: user.stop(finished.n)), "a closed dump is not stopped again"
    assert "is closed" in refused(lambda: agent.ask(finished.n, "Why?")), "a closed dump asks nothing"
    user.remove(finished.n)
    assert "already removed" in refused(lambda: user.remove(finished.n)) and "only the user removes" in refused(lambda: agent.remove(finished.n)), \
        "a dump is removed once, and only by the user"


def test_a_closed_dump_lets_go_of_the_drafts_it_still_held():
    from controllers.stored import DRAFT_OF
    from migrations.m0050_dump_drafts_released import run

    record = fresh()
    dumps = CONTROLLERS["dump"](record, actor=AGENT)
    dump = dumps.create("Old dump")
    held = Todos(record, actor=AGENT).create("held draft", **{DRAFT_OF: dump.ref})
    other = Todos(record, actor=AGENT).create("someone else's", **{DRAFT_OF: "dump:99"})
    dumps.link(dump.n, held.ref)
    dumps.link(dump.n, other.ref)
    dumps.link(dump.n, "todo:77")
    dumps.link(dump.n, "mystery:1")
    dumps.complete(dump.n, how="filed")
    assert run(record.root) == [held.ref], "only a row still held by this dump is released"
    assert not Todos(record, actor=AGENT).load(held.n).data.get(DRAFT_OF), "the released row no longer names the dump"
    assert Todos(record, actor=AGENT).load(other.n).data[DRAFT_OF] == "dump:99", "a row held by another dump stays held"
