from controllers.types import Environments, Messages, Todos
from engine.record import Record
from features import load
from features.format import VIEWER, shaped
from resources.base import USER
from tests.conftest import fresh


def test_a_row_named_in_text_is_a_chip_in_the_viewer_and_plain_words_everywhere_else():
    load()
    record = fresh()
    todos, messages = Todos(record, actor=USER), Messages(record, actor=USER)
    first, second, third = (messages.create(f"note {i}").n for i in range(3))
    row = todos.create("see to-do 648", brief=f"answered message {first}, not `todo 5`, and message 999 is gone")
    viewer = shaped(todos.load(row.n), record, VIEWER)
    assert (viewer["title"], viewer["brief"]) == \
        ("see to-do 648", f"answered [[chip message:{first}|message {first}]], not `todo 5`, and message 999 is gone"), \
        "the viewer gets each row named in a brief marked as a chip, code left alone, and a number with no row stays plain"
    assert shaped(todos.load(row.n), record)["brief"] == f"answered message {first}, not `todo 5`, and message 999 is gone", \
        "the command line gets the plain words"
    todos.update(row.n, brief=viewer["brief"])
    assert todos.load(row.n).brief.startswith(f"answered message {first},"), "a marker sent back by the viewer is saved as plain words"
    listed = todos.create("reviews", brief=f"messages {first}, {second} and {third}")
    assert shaped(todos.load(listed.n), record, VIEWER)["brief"] == \
        f"[[chip message:{first}|message {first}]], [[chip message:{second}|message {second}]] and [[chip message:{third}|message {third}]]", \
        "several numbers after one type name are one chip each, each naming its row"
    Environments(record, actor=USER).create("ticket-11")
    theirs = Todos(Record(record.root, "ticket-11"), actor=USER).create("their row").n
    pointed = todos.create("elsewhere", brief=f"ticket-11 to-do {theirs} and to-do {theirs} in ticket-11")
    assert shaped(todos.load(pointed.n), record, VIEWER)["brief"] == \
        f"[[chip todo:{theirs}@ticket-11|ticket-11 to-do {theirs}]] and [[chip todo:{theirs}@ticket-11|to-do {theirs} in ticket-11]]", \
        "a row named with its environment is a chip that opens it there"
    spanned = todos.create("coded", brief=f"`message {first}` and `journal message read {first}`")
    assert shaped(todos.load(spanned.n), record, VIEWER)["brief"] == \
        f"[[chip message:{first}|message {first}]] and `journal message read {first}`", \
        "a reference alone in code is a chip, and a command in code stays code"


def test_files_commits_and_links_are_marked_by_the_server_and_code_is_left_alone():
    load()
    record = fresh()
    row = Todos(record, actor=USER).create("paths", brief="See engine/hooks.py, commit 4b64ddbb7 and https://example.com/a. `engine/x.py` stays")
    viewed = shaped(row, record, VIEWER)["brief"]
    assert "[[file engine/hooks.py|hooks.py]]" in viewed and "[[commit 4b64ddbb7|4b64ddb]]" in viewed, "a path and a commit get their markers"
    assert "[[url https://example.com/a|https://example.com/a]]" in viewed, "a link gets its marker"
    assert "`engine/x.py`" in viewed, "a path in code is left as code"
    assert shaped(row, record)["brief"] == row.brief, "outside the viewer the text stays plain"


def test_a_path_in_text_names_a_file_only_when_it_is_in_the_project():
    from features.row_links.paths import existing
    record = fresh()
    project = record.root.parent
    (project / "web").mkdir()
    (project / "web" / "Turn.vue").write_text("")
    assert (existing(project, "web/Turn.vue"), existing(project, "web/Missing.vue")) == ("web/Turn.vue", ""), "a path is kept when the file is there and dropped when it is not"


def test_a_file_chip_into_another_project_cannot_read_its_files():
    from commands.http import dispatch
    record = fresh()
    other = record.root.parent.parent / "other-project"
    (other / ".git").mkdir(parents=True)
    (other / "composer.json").write_text("{}\n")
    (other / ".env").write_text("SECRET=1\n")
    got = dispatch("GET", f"/api/{record.env}/file", record.root, {"path": str(other / "composer.json")}, {})
    hidden = dispatch("GET", f"/api/{record.env}/file", record.root, {"path": str(other / ".env")}, {})
    assert got.code == 400
    assert hidden.code == 400


def test_a_file_name_is_a_chip_when_one_project_file_has_it_and_the_agent_hears_of_a_shared_one():
    from controllers.types import Agents, Nudges
    from engine import chat
    from resources.base import SYSTEM
    from tests.kit import report
    load()
    record = fresh()
    project = record.root.parent
    for path in ("web/Turn.vue", "a/test.py", "b/test.py"):
        (project / path).parent.mkdir(parents=True, exist_ok=True)
        (project / path).write_text("x")
    from engine.project_files import walk
    walk(project)
    row = Todos(record, actor=USER).create("files", brief="Turn.vue, test.py, `web/Turn.vue` and `web/Gone.vue`")
    viewed = shaped(row, record, VIEWER)["brief"]
    assert viewed.count("[[file web/Turn.vue|") == 2, f"a unique name and an existing path in code both open the file: {viewed}"
    assert "/test.py|" not in viewed and "`web/Gone.vue`" in viewed, f"a shared name and a missing path stay text: {viewed}"
    twins = shaped(Todos(record, actor=USER).create("twins", brief="a/test.py:22 and b/test.py and web/Turn.vue:8-12"), record, VIEWER)["brief"]
    chips = ("[[file a/test.py#L22|a/test.py:22]]", "[[file b/test.py|b/test.py]]", "[[file web/Turn.vue#L8|Turn.vue:8-12]]")
    assert all(chip in twins for chip in chips), f"a chip is the file name, with folders only where names repeat, and keeps its line: {twins}"
    one, two = (Todos(record, actor=USER).create(f"landed {i}").n for i in range(2))
    bare = shaped(Todos(record, actor=USER).create("bare", brief=f"landed (to-dos {one}, {two}) and (see to-do 2)"), record, VIEWER)["brief"]
    assert bare == f"landed [[chip todo:{one}|to-do {one}]] and [[chip todo:{two}|to-do {two}]] and (see [[chip todo:2|to-do 2]])", \
        f"parentheses around nothing but chips go: {bare}"
    report(record, "working", "PostToolUse")
    chat.send(record, Agents(record, actor=SYSTEM).by_session("claude-1"), "the fix is in test.py")
    nudged = [n.title for n in Nudges(record).all() if "test.py" in n.title]
    assert nudged == ["test.py names 2 files in the project"], f"the agent is nudged to write the path: {nudged}"
    for nudge in Nudges(record).all():
        Nudges(record).delete(nudge.n, "cleared")
    chat.send(record, Agents(record, actor=SYSTEM).by_session("claude-1"), "again: test.py")
    assert [n.title for n in Nudges(record).all() if "test.py" in n.title] == [], "naming the same shared file a second time does not tell the agent again"


def test_cold_project_indexing_is_shared_while_the_viewer_keeps_answering():
    import threading
    from engine import project_files
    from resources.base import AGENT

    load()
    record = fresh()
    project = record.root.parent
    cold_file = project / "ColdWidget.vue"
    cold_file.write_text("<template />")
    project_files.WALKED.pop(str(project), None)
    project_files.WALKING.discard(str(project))
    entered, release, published = threading.Event(), threading.Event(), threading.Event()
    actual_walk, walks, scan_threads, scan_errors = project_files.walk, [], [], []

    def held_walk(path):
        if path == project:
            walks.append(path)
            scan_threads.append(threading.current_thread())
            entered.set()
            release.wait()
        try:
            result = actual_walk(path)
        except BaseException as error:
            scan_errors.append(error)
            raise
        if path == project:
            published.set()
        return result

    project_files.walk = held_walk
    workers, results, worker_errors = [], [None, None], []

    def format_row(row, index):
        try:
            results[index] = shaped(row, record, VIEWER)
        except BaseException as error:
            worker_errors.append(error)
            raise

    try:
        rows = [Todos(record, actor=AGENT).create(f"cold {i}", brief="ColdWidget.vue") for i in range(2)]
        workers = [threading.Thread(target=format_row, args=(row, index)) for index, row in enumerate(rows)]
        for worker in workers:
            worker.start()
        assert entered.wait(5), "the native project walk started"
        for worker in workers:
            worker.join(5)
        assert not worker_errors and not scan_errors, f"formatter and scan workers completed without errors: {worker_errors + scan_errors}"
        assert all(not worker.is_alive() for worker in workers), "viewer formatting answers while indexing is held"
        assert all(result is not None for result in results), f"both formatter requests returned: {results}"
        assert all("[[file" not in result["brief"] for result in results), "cold responses do not invent a file chip"
        release.set()
        assert published.wait(5), "the native project walk publishes after release"
        resolved = Todos(record, actor=AGENT).create("resolved", brief="ColdWidget.vue")
        assert shaped(resolved, record, VIEWER)["brief"] == "[[file ColdWidget.vue|ColdWidget.vue]]", "published indexing resolves the actual file"
        assert len(walks) == 1, "overlapping viewer requests share one native walk"
    finally:
        release.set()
        for worker in workers:
            worker.join(5)
        for scan_thread in scan_threads:
            scan_thread.join(5)
        finished = all(not worker.is_alive() for worker in workers) and all(not thread.is_alive() for thread in scan_threads)
        project_files.walk = actual_walk
        assert finished, "request and native scan threads finish before the wrapper is restored"


def test_a_setting_change_leaves_formatted_rows_cached_unless_a_formatter_reads_it():
    from controllers.types import Features
    from resources.base import SYSTEM
    load()
    record = fresh()
    todos = Todos(record, actor=USER)
    row = todos.create("see message 1", brief="a brief with no path, no hash and no number")
    first = shaped(todos.load(row.n), record, VIEWER)
    Features(record, actor=SYSTEM).configure("helpers", "kept", "9")
    assert shaped(todos.load(row.n), record, VIEWER) is first, "a setting no formatter reads leaves the formatted row cached"
    Features(record, actor=SYSTEM).switch("row_links", False)
    assert shaped(todos.load(row.n), record, VIEWER) is not first, "switching off a feature that owns a formatter formats it again"
    from features.row_links.formatters import MarkRows
    from features.row_links.paths import MarkPaths
    from features.parts import Context
    import features
    here = Context.of(features.FEATURES["row_links"], record)
    assert (MarkRows().format(here, "no digits here"), MarkPaths().format(here, "plain words, nothing linked")) == ("no digits here", "plain words, nothing linked"), \
        "a text with no digit, address, file ending or hash goes through the link formatters unchanged"
