import time


from controllers.types import Notifications, Nudges, Reports, Todos
from resources.base import AGENT, SYSTEM, USER, Refused
from tests.kit import tick
from tests.conftest import fresh


def age(c, n, **fields):
    r = c.load(n)
    for k, v in fields.items():
        setattr(r, k, v)
    c.path(n).write_text(r.dump())


def test_reports_and_todos_past_their_keep_days_are_archived_open_ones_never():
    record = fresh()
    reports = Reports(record, actor=AGENT)
    todos = Todos(record, actor=USER)
    fresh_report = reports.create("checked today")
    old_report = reports.create("checked a month ago")
    age(reports, old_report.n, created=time.time() - 30 * 86400)
    open_row = todos.create("still open")
    done_row = todos.create("done long ago")
    todos.complete(done_row.n, "done")
    age(todos, done_row.n, completed=time.time() - 10 * 86400)
    tick(record)
    assert (bool(reports.load(old_report.n).completed), reports.load(old_report.n).outcome) == (True, "aged out after 14 days"), \
        "a report past its keep days is archived, saying so"
    assert reports.load(fresh_report.n).completed == 0.0, "a fresh report stays"
    assert ([t.n for t in todos.all()], [e.data["why"] for e in record.event_log.events() if e.type == "todo" and e.action == "deleted"]) == \
        ([open_row.n], ["archived 7 days after it was closed"]), "a to-do closed past its keep days is archived, an open one never"


def test_a_type_that_declares_how_many_rows_it_keeps_is_pruned_to_that_count(monkeypatch):
    from resources.types import Notification, Nudge
    monkeypatch.setattr(Notification, "kept", 1)
    monkeypatch.setattr(Nudge, "kept", 1)
    record = fresh()
    notes = Notifications(record, actor=AGENT)
    seen_old = notes.create("told long ago")
    unseen_old = notes.create("never looked at")
    seen_new = notes.create("told today")
    for n in (seen_old.n, seen_new.n):
        Notifications(record, actor=USER).read(n)
    age(notes, seen_old.n, created=time.time() - 2 * 86400)
    nudges = Nudges(record, actor=SYSTEM)
    old_line, new_line = nudges.create("said a while ago"), nudges.create("said just now")
    age(nudges, old_line.n, created=time.time() - 60)
    tick(record)
    assert sorted(r.n for r in notes.all(deleted=True, completed=True)) == [unseen_old.n, seen_new.n], \
        "past the count, the oldest seen notification goes; an unseen one is never pruned"
    assert [r.n for r in nudges.all(deleted=True, completed=True)] == [new_line.n], "past the count, the oldest nudge goes"


def test_keep_zero_leaves_reports_listed():
    kept = fresh()
    kept.set_setting("keep", {"report": 0})
    r = Reports(kept, actor=AGENT).create("kept forever")
    age(Reports(kept), r.n, created=time.time() - 300 * 86400)
    tick(kept)
    assert Reports(kept).load(r.n).completed == 0.0, "keep 0 leaves reports listed"
    kept.set_setting("keep", {"report": 0, "pack": 0})
    tick(kept)
    assert not any((kept.home / "report" / "packed").glob("*.zip")), "packing nothing is what keep 0 asks for the packing of closed rows"


def test_closed_rows_are_packed_into_a_zip_and_still_read_listed_reopened_and_removed():
    record = fresh()
    todos = Todos(record, actor=SYSTEM)
    rows = [todos.create(f"row {i}") for i in range(3)]
    for r in rows[:2]:
        todos.complete(r.n, how="done")
    folder = todos.path(rows[0].n).parent
    assert todos.rows.pack(time.time() + 1) == 2, "only the closed rows are packed"
    assert (todos.path(rows[0].n).exists(), any((folder / "packed").glob("????-??-??.zip"))) == (False, True), "their files are gone into the day's zip"
    assert [r.title for r in (todos.load(rows[0].n), todos.load(rows[1].n))] == ["row 0", "row 1"], "a packed row reads straight from the zip"
    assert [row["n"] for row in todos.rows.summaries()] == [r.n for r in rows], "and is still listed"
    from tests.kit import dispatch
    shown = dispatch("GET", f"/api/{record.env}/todo", record.root, {"completed": "1"}, {}).body["rows"]
    assert [row["title"] for row in shown] == ["row 0", "row 1", "row 2"], "and the viewer lists it, read from the zip"
    assert todos.create("next").n == rows[2].n + 1, "a new row never takes a packed row's number"
    todos.reopen(rows[0].n, "again")
    assert (todos.path(rows[0].n).exists(), todos.load(rows[0].n).completed, [row["n"] for row in todos.rows.summaries()].count(rows[0].n)) == (True, 0.0, 1), \
        "a packed row that changes is loose again, listed once"
    todos.complete(rows[0].n, how="done again")
    assert todos.rows.pack(time.time() + 1) == 1 and todos.load(rows[0].n).completed and todos.load(rows[1].n).title == "row 1", \
        "packing into the same day again keeps the zip readable, with the rows already in it"
    todos.force_delete(rows[1].n)
    try:
        todos.load(rows[1].n)
        raise AssertionError("a removed packed row is gone")
    except Refused:
        pass
    gone = todos.create("a row whose file went missing")
    todos.complete(gone.n, how="done")
    todos.rows.summaries()
    todos.path(gone.n).unlink()
    assert todos.rows.pack(time.time() + 1) == 0, "a row the index still lists but whose file is gone is left out of the pack, never read"


def test_packing_refuses_a_zip_that_does_not_read_back_and_a_damaged_zip_refuses_in_words(monkeypatch):
    import zipfile
    record = fresh()
    todos = Todos(record, actor=SYSTEM)
    row = todos.create("a row to pack")
    todos.complete(row.n, how="done")
    folder = todos.path(row.n).parent
    with monkeypatch.context() as bad:
        bad.setattr(zipfile.ZipFile, "read", lambda self, name: b"not what was written")
        try:
            todos.rows.pack(time.time() + 1)
            raise AssertionError("a zip that reads back wrong is refused")
        except OSError as error:
            assert "did not read back" in str(error), "the refusal says the zip did not read back as written"
    assert (todos.path(row.n).is_file(), list((folder / "packed").glob("*.zip")), todos.load(row.n).title) == (True, [], "a row to pack"), \
        "a wrong read-back keeps the loose file and leaves no zip"
    assert todos.rows.pack(time.time() + 1) == 1, "the row packs on the next try"
    (archive,) = (folder / "packed").glob("*.zip")
    archive.write_bytes(b"this is not a zip")
    try:
        todos.load(row.n)
        raise AssertionError("a damaged zip is refused")
    except (Refused, zipfile.BadZipFile) as error:
        assert isinstance(error, Refused) and "missing from" in str(error), "a damaged zip refuses in words on load, naming the zip"
