from __future__ import annotations

import json
import multiprocessing as mp
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

import pytest

from aicd.tools.code_gen_tree import (
    build_scan_payload,
    python_file_gen_tree,
    write_gen_tree_json,
)
from aicd.tools.code_gen_tree_query import query_tree
from aicd.tools.code_gen_tree_store import (
    _versioned_tree_file_lock,
    conventional_slug_dir,
    manifest_path,
    read_manifest,
    read_manifest_for_use,
    write_versioned_tree,
)


def _mp_versioned_write(args: tuple[str, str, str, int]) -> int:
    """Pool worker（spawn）：必须模块顶层以便 Windows 可 pickle。"""
    ai_out_s, slug, src_s, tag = args
    ai_out = Path(ai_out_s)
    src = Path(src_s)
    from aicd.tools.code_gen_tree import build_scan_payload
    from aicd.tools.code_gen_tree_store import write_versioned_tree

    pl = build_scan_payload(src, max_files=10)
    pl2 = {**pl, "nodes": list(pl["nodes"]), "mp": tag}
    st = write_versioned_tree(ai_out, slug, pl2, scan_root=src_s)
    return int(st["index_revision"])


def test_python_file_nested(tmp_path: Path) -> None:
    p = tmp_path / "m.py"
    p.write_text(
        '"""mod doc."""\n'
        "class A:\n"
        '    """cla."""\n'
        "    def m(self):\n"
        "        def inner():\n"
        "            pass\n"
        "        pass\n",
        encoding="utf-8",
    )
    out = python_file_gen_tree(p)
    assert out["ok"] is True
    kinds = [n["kind"] for n in out["nodes"]]
    assert kinds[0] == "module"
    assert "class" in kinds
    assert "function" in kinds
    qnames = {n["qname"] for n in out["nodes"]}
    # module_qname defaults to file stem → "m" for m.py
    assert "m.A.m.inner" in qnames
    mod = out["nodes"][0]
    assert mod["parent_id"] is None
    assert mod["docstring_preview"] == "mod doc."
    assert mod.get("parser") == "ast"


def test_build_scan_payload_dir(tmp_path: Path) -> None:
    pkg = tmp_path / "pkg"
    pkg.mkdir()
    (pkg / "__init__.py").write_text("# x\n", encoding="utf-8")
    (pkg / "util.py").write_text("def f():\n    pass\n", encoding="utf-8")
    pl = build_scan_payload(tmp_path, recursive=True, max_files=50)
    assert pl["ok"] is True
    assert pl["files_scanned"] == 2
    qn = {n["qname"] for n in pl["nodes"]}
    assert "pkg.util.f" in qn
    assert "python" in pl["languages"]


def test_write_gen_tree_json(tmp_path: Path) -> None:
    p = tmp_path / "a.py"
    p.write_text("def x():\n    pass\n", encoding="utf-8")
    payload = build_scan_payload(p, max_files=10)
    dest = tmp_path / "tree.json"
    wr = write_gen_tree_json(payload, dest)
    assert wr["ok"] is True
    data = json.loads(dest.read_text(encoding="utf-8"))
    assert data["schema"] == "aicd_code_gen_tree"
    assert "generated_at" in data


def test_go_polyglot(tmp_path: Path) -> None:
    (tmp_path / "main.go").write_text(
        "package main\n\nfunc main() {}\n",
        encoding="utf-8",
    )
    pl = build_scan_payload(tmp_path, max_files=10, scan_profile="polyglot")
    assert pl["ok"] is True
    assert "go" in pl["languages"]
    qn = {n["qname"] for n in pl["nodes"]}
    assert any("main" in q for q in qn)


def test_manifest_repair_after_corrupt_manifest(tmp_path: Path) -> None:
    ai_out = tmp_path / "aicd"
    src = tmp_path / "src"
    src.mkdir()
    (src / "a.py").write_text("def f():\n    pass\n", encoding="utf-8")
    pl = build_scan_payload(src, max_files=10)
    write_versioned_tree(ai_out, "rep", pl, scan_root=str(src))
    mp = manifest_path(ai_out, "rep")
    mp.write_text("{not json", encoding="utf-8")
    man, issues = read_manifest_for_use(ai_out, "rep", attempt_repair=True)
    assert man is not None
    assert man.get("latest_revision") == 1
    assert "manifest_repaired_from_disk" in issues


def test_manifest_no_repair_returns_none_when_broken(tmp_path: Path) -> None:
    ai_out = tmp_path / "aicd"
    src = tmp_path / "src"
    src.mkdir()
    (src / "a.py").write_text("x=1\n", encoding="utf-8")
    pl = build_scan_payload(src, max_files=10)
    write_versioned_tree(ai_out, "nr", pl, scan_root=str(src))
    manifest_path(ai_out, "nr").write_text("{}", encoding="utf-8")
    man, issues = read_manifest_for_use(ai_out, "nr", attempt_repair=False)
    assert man is None
    assert any("manifest" in x for x in issues)


def test_write_versioned_tree_lock_timeout(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    ai_out = tmp_path / "aicd"
    src = tmp_path / "src2"
    src.mkdir()
    (src / "b.py").write_text("y=2\n", encoding="utf-8")
    base = conventional_slug_dir(ai_out, "to")
    base.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(
        "aicd.tools.code_gen_tree_store._lock_timeout_sec",
        lambda: 0.12,
    )
    ev = threading.Event()

    def _hold() -> None:
        lk = _versioned_tree_file_lock(base)
        lk.acquire()
        ev.set()
        time.sleep(0.9)
        lk.release()

    threading.Thread(target=_hold, daemon=True).start()
    assert ev.wait(timeout=2.0)
    pl = build_scan_payload(src, max_files=5)
    st = write_versioned_tree(ai_out, "to", pl, scan_root=str(src))
    assert st.get("ok") is False
    assert st.get("code") == "lock_timeout"


def test_write_versioned_tree_serializes_concurrent_writes(tmp_path: Path) -> None:
    """同一 topic_slug 多线程并发 sync 时 revision 单调递增（filelock）。"""
    ai_out = tmp_path / "aicd"
    src = tmp_path / "src"
    src.mkdir()
    (src / "a.py").write_text("def foo():\n    pass\n", encoding="utf-8")
    base_payload = build_scan_payload(src, max_files=10)
    n = 12

    def _one(i: int) -> int:
        pl = dict(base_payload)
        pl["nodes"] = list(base_payload["nodes"])
        pl["concurrent_test_tag"] = i
        st = write_versioned_tree(ai_out, "locktopic", pl, scan_root=str(src))
        return int(st["index_revision"])

    with ThreadPoolExecutor(max_workers=8) as ex:
        futs = [ex.submit(_one, i) for i in range(n)]
        revs = sorted([f.result() for f in as_completed(futs)])

    assert revs == list(range(1, n + 1))
    man = read_manifest(ai_out, "locktopic")
    assert man is not None
    assert man["latest_revision"] == n


def test_write_versioned_tree_serializes_multiprocess(tmp_path: Path) -> None:
    """同一 topic_slug 多进程 spawn 池并发 sync：revision 单调且不损坏 manifest。"""
    ctx = mp.get_context("spawn")
    ai_out = tmp_path / "aicd"
    src = tmp_path / "src"
    src.mkdir(parents=True)
    (src / "a.py").write_text("def x():\n    pass\n", encoding="utf-8")
    n = 4
    with ctx.Pool(n) as pool:
        revs = pool.map(
            _mp_versioned_write,
            [(str(ai_out), "mpslug", str(src), i) for i in range(n)],
        )
    assert sorted(revs) == list(range(1, n + 1))
    man = read_manifest(ai_out, "mpslug")
    assert man is not None
    assert man["latest_revision"] == n


def test_versioned_store_and_query(tmp_path: Path) -> None:
    ai_out = tmp_path / "aicd"
    src = tmp_path / "src"
    src.mkdir()
    (src / "a.py").write_text("def foo():\n    pass\n", encoding="utf-8")
    payload = build_scan_payload(src, max_files=10)
    st = write_versioned_tree(ai_out, "mytopic", payload, scan_root=str(src))
    assert st["index_revision"] == 1
    man = read_manifest(ai_out, "mytopic")
    assert man is not None
    assert man["latest_revision"] == 1
    tree_path = Path(st["tree_json_path"])
    stats = query_tree(tree_path, action="stats")
    assert stats["ok"] is True
    assert stats["meta"]["total_nodes"] >= 2
    st2 = write_versioned_tree(ai_out, "mytopic", payload, scan_root=str(src))
    assert st2["index_revision"] == 2


def test_query_children(tmp_path: Path) -> None:
    p = tmp_path / "x.py"
    p.write_text("class A:\n    def m(self):\n        pass\n", encoding="utf-8")
    payload = build_scan_payload(p, max_files=5)
    dest = tmp_path / "t.json"
    write_gen_tree_json(payload, dest)
    data = json.loads(dest.read_text(encoding="utf-8"))
    mod_id = next(n["id"] for n in data["nodes"] if n["kind"] == "module")
    ch = query_tree(dest, action="children", parent_id=mod_id, limit=50)
    assert ch["ok"] is True
    assert ch["returned"] >= 1
