from pathlib import Path

import pytest

from heta.config.schema import (
    DynamicInsertConfig,
    InsertPlanningConfig,
    HetaConfig,
    LLMConfig,
    MinerUConfig,
    VectorIndexConfig,
)
from heta.kb.discovery import collect_insert_files
from heta.kb.models import FileChange, ParsedDocument
from heta.kb.insert import _ensure_code_raw_links, insert_paths
from heta.kb.text import frontmatter_page, slugify, summarize
from heta.kb.wiki import normalize_wiki_pages, repair_broken_wiki_links


def _config(mineru: MinerUConfig | None = None, *, dynamic_insert: bool = True) -> HetaConfig:
    return HetaConfig(
        version=1,
        llm=LLMConfig(provider="qwen", api_key="sk-test"),
        mineru=mineru or MinerUConfig.disabled(),
        vector_index=VectorIndexConfig(enable=False),
        insert_planning=InsertPlanningConfig.enabled(),
        dynamic_insert=DynamicInsertConfig(enable=dynamic_insert),
    )


def _fake_agent(monkeypatch, calls: list[list[str]] | None = None) -> None:
    def run_merge_agent(*, task_id, documents, root_dir, config):
        assert len(documents) == 1
        if calls is not None:
            calls.append([document.source_name for document in documents])
        pages = root_dir / "pages"
        pages.mkdir(parents=True, exist_ok=True)
        added = []
        updated = []
        for document in documents:
            slug = slugify(document.title)
            page = next(
                (
                    candidate
                    for candidate in pages.glob("*.md")
                    if candidate.name.endswith(f"-{slug}.md") or candidate.name == f"{slug}.md"
                ),
                pages / f"{slug}.md",
            )
            rel = f"pages/{page.name}"
            content = frontmatter_page(
                document.title,
                document.source_name,
                summarize(document.markdown_content),
                document.markdown_content,
            )
            if page.exists():
                page.write_text(page.read_text(encoding="utf-8") + "\n## Imported Update\n", encoding="utf-8")
                updated.append(FileChange("updated", document.title, rel))
            else:
                page.write_text(content, encoding="utf-8")
                added.append(FileChange("added", document.title, rel))
            index = root_dir / "index.md"
            index.write_text(
                index.read_text(encoding="utf-8").rstrip()
                + f"\n\n## Imported Knowledge\n\n- [[{document.title}]] ({rel})\n  - {summarize(document.markdown_content)}\n",
                encoding="utf-8",
            )
            with (root_dir / "log.md").open("a", encoding="utf-8") as log:
                log.write(f"- Created page: {document.title} from {document.source_name}\n")
        return {"added": added, "updated": updated, "deleted": []}

    monkeypatch.setattr("heta.kb.insert.run_merge_agent", run_merge_agent)


def test_insert_markdown_creates_versioned_wiki(monkeypatch, tmp_path: Path) -> None:
    _fake_agent(monkeypatch)
    source = tmp_path / "notes.md"
    source.write_text("# Neural Network Basics\n\nLayers and activations.", encoding="utf-8")

    result = insert_paths([source], _config(), base_dir=tmp_path)

    wiki = tmp_path / "workspace" / "kb" / "wiki"
    raw = tmp_path / "workspace" / "kb" / "raw"
    page = wiki / "pages" / "1-neural-network-basics.md"

    assert result.commit_id
    assert page.exists()
    assert (wiki / ".git").exists()
    assert "[[Neural Network Basics]]" in (wiki / "index.md").read_text(encoding="utf-8")
    assert "Created page: Neural Network Basics" in (wiki / "log.md").read_text(encoding="utf-8")
    assert list(raw.glob("*notes.md"))


def test_insert_same_title_updates_existing_page(monkeypatch, tmp_path: Path) -> None:
    _fake_agent(monkeypatch)
    first = tmp_path / "first.md"
    second = tmp_path / "second.md"
    first.write_text("# Shared Topic\n\nFirst version.", encoding="utf-8")
    second.write_text("# Shared Topic\n\nSecond version.", encoding="utf-8")

    insert_paths([first], _config(), base_dir=tmp_path)
    result = insert_paths([second], _config(), base_dir=tmp_path)

    page = tmp_path / "workspace" / "kb" / "wiki" / "pages" / "1-shared-topic.md"
    assert result.updated[0].title == "Shared Topic"
    assert result.updated[0].path == "pages/1-shared-topic.md"
    assert "## Imported Update" in page.read_text(encoding="utf-8")


def test_insert_multiple_files_runs_agent_sequentially(monkeypatch, tmp_path: Path) -> None:
    calls: list[list[str]] = []
    progress = []
    _fake_agent(monkeypatch, calls)
    first = tmp_path / "alpha.md"
    second = tmp_path / "beta.md"
    first.write_text("# Alpha\n\nFirst details.", encoding="utf-8")
    second.write_text("# Beta\n\nSecond details.", encoding="utf-8")

    result = insert_paths(
        [first, second],
        _config(),
        base_dir=tmp_path,
        on_progress=progress.append,
    )

    wiki = tmp_path / "workspace" / "kb" / "wiki"
    assert calls[0][0].endswith("_alpha.md")
    assert calls[1][0].endswith("_beta.md")
    assert (wiki / "pages" / "1-alpha.md").exists()
    assert (wiki / "pages" / "2-beta.md").exists()
    assert [change.path for change in result.added] == ["pages/1-alpha.md", "pages/2-beta.md"]
    assert progress[0].percent == 1
    merge_percents = [event.percent for event in progress if event.phase == "merge"]
    assert 50 in merge_percents
    assert 99 in merge_percents
    assert progress[-1].percent == 100
    assert progress[-1].phase == "done"


def test_insert_defaults_to_static_pages(monkeypatch, tmp_path: Path) -> None:
    def fail_agent(**kwargs):
        raise AssertionError("dynamic agent should not run in static insert mode")

    monkeypatch.setattr("heta.kb.insert.run_merge_agent", fail_agent)
    monkeypatch.setattr(
        "heta.kb.static_insert.generate_summary",
        lambda *, document, config: f"Summary for {document.title}.",
    )
    source = tmp_path / "manual.md"
    source.write_text("# Main Heading\n\n## Sub Heading\n\nBody text.", encoding="utf-8")

    result = insert_paths([source], _config(dynamic_insert=False), base_dir=tmp_path)

    wiki = tmp_path / "workspace" / "kb" / "wiki"
    page = wiki / "pages" / "1-main-heading.md"
    text = page.read_text(encoding="utf-8")
    assert result.added[0].path == "pages/1-main-heading.md"
    assert "Summary for Main Heading." in text
    assert "### Main Heading" in text
    assert "#### Sub Heading" in text
    assert "## Related Pages\n\n- None yet" in text
    assert "- " + result.raw_files[0].name in text
    assert "[[Main Heading]]" in (wiki / "index.md").read_text(encoding="utf-8")
    assert "Created static page: Main Heading" in (wiki / "log.md").read_text(encoding="utf-8")


def test_insert_reports_vector_sync_error_without_rolling_back(monkeypatch, tmp_path: Path) -> None:
    monkeypatch.setattr(
        "heta.kb.static_insert.generate_summary",
        lambda *, document, config: f"Summary for {document.title}.",
    )
    monkeypatch.setattr(
        "heta.kb.insert.sync_wiki_vector_index",
        lambda **kwargs: (_ for _ in ()).throw(RuntimeError("embedding unavailable")),
    )
    source = tmp_path / "manual.md"
    source.write_text("# Main Heading\n\nBody text.", encoding="utf-8")
    config = HetaConfig(
        version=1,
        llm=LLMConfig(provider="qwen", api_key="sk-test"),
        mineru=MinerUConfig.disabled(),
        vector_index=VectorIndexConfig(enable=True),
        insert_planning=InsertPlanningConfig.enabled(),
        dynamic_insert=DynamicInsertConfig.disabled(),
    )

    result = insert_paths([source], config, base_dir=tmp_path)

    page = tmp_path / "workspace" / "kb" / "wiki" / "pages" / "1-main-heading.md"
    assert result.commit_id
    assert page.exists()
    assert result.vector_index_error == "embedding unavailable"


def test_insert_continues_when_agent_makes_no_wiki_changes(monkeypatch, tmp_path: Path) -> None:
    calls: list[str] = []

    def run_merge_agent(*, task_id, documents, root_dir, config):
        document = documents[0]
        calls.append(document.source_name)
        if "beta" in document.source_name:
            return {"added": [], "updated": [], "deleted": []}

        pages = root_dir / "pages"
        pages.mkdir(parents=True, exist_ok=True)
        page = pages / f"{slugify(document.title)}.md"
        page.write_text(
            frontmatter_page(
                document.title,
                document.source_name,
                summarize(document.markdown_content),
                document.markdown_content,
            ),
            encoding="utf-8",
        )
        return {"added": [FileChange("added", document.title, f"pages/{page.name}")], "updated": [], "deleted": []}

    monkeypatch.setattr("heta.kb.insert.run_merge_agent", run_merge_agent)
    first = tmp_path / "alpha.md"
    second = tmp_path / "beta.md"
    third = tmp_path / "gamma.md"
    first.write_text("# Alpha\n\nFirst details.", encoding="utf-8")
    second.write_text("# Beta\n\nSecond details.", encoding="utf-8")
    third.write_text("# Gamma\n\nThird details.", encoding="utf-8")

    result = insert_paths([first, second, third], _config(), base_dir=tmp_path)

    wiki = tmp_path / "workspace" / "kb" / "wiki"
    assert len(calls) == 3
    assert [change.path for change in result.added] == ["pages/1-alpha.md", "pages/2-gamma.md"]
    assert result.skipped_documents == [calls[1]]
    assert (wiki / "pages" / "1-alpha.md").exists()
    assert not (wiki / "pages" / "2-beta.md").exists()
    assert (wiki / "pages" / "2-gamma.md").exists()
    assert "Skipped no-op merge" not in (wiki / "log.md").read_text(encoding="utf-8")


def test_ensure_code_raw_links_restores_agent_dropped_raw_link(tmp_path: Path) -> None:
    wiki = tmp_path / "wiki"
    page = wiki / "pages" / "1-code-demo.md"
    page.parent.mkdir(parents=True)
    page.write_text(
        frontmatter_page("Code - demo.py", "2026-05-15_demo.py", "Summary.", "### File Overview\n- language: python"),
        encoding="utf-8",
    )
    document = ParsedDocument(
        source_path=tmp_path / "demo.py",
        archived_path=tmp_path / "raw" / "2026-05-15_demo.py",
        title="Code - demo.py",
        markdown_content="",
        source_name="2026-05-15_demo.py",
        metadata={"extension": ".py"},
    )

    _ensure_code_raw_links(wiki, document, [FileChange("added", "Code - demo.py", "pages/1-code-demo.md")])

    assert "[Raw source](<../../raw/2026-05-15_demo.py>)" in page.read_text(encoding="utf-8")


def test_pdf_requires_mineru_when_disabled(tmp_path: Path) -> None:
    source = tmp_path / "paper.pdf"
    source.write_bytes(b"%PDF")

    with pytest.raises(ValueError, match="requires MinerU"):
        collect_insert_files([source], _config())


def test_office_requires_mineru_when_disabled(tmp_path: Path) -> None:
    source = tmp_path / "deck.pptx"
    source.write_bytes(b"pptx")

    with pytest.raises(ValueError, match="requires MinerU"):
        collect_insert_files([source], _config())


def test_collect_insert_files_accepts_office_when_mineru_enabled(tmp_path: Path) -> None:
    files = []
    for name in ["notes.doc", "notes.docx", "deck.ppt", "deck.pptx", "sheet.xls", "sheet.xlsx"]:
        file = tmp_path / name
        file.write_bytes(b"office")
        files.append(file)

    collected = collect_insert_files(
        [tmp_path],
        _config(MinerUConfig(enable=True, provider="cloud", api_key="mineru-token", endpoint=None)),
    )

    assert collected == sorted(files)


def test_collect_insert_files_accepts_common_images(tmp_path: Path) -> None:
    image = tmp_path / "diagram.png"
    image.write_bytes(b"png")

    files = collect_insert_files([image], _config())

    assert files == [image]


def test_collect_insert_files_accepts_audio_and_video(tmp_path: Path) -> None:
    audio = tmp_path / "meeting.mp3"
    video = tmp_path / "demo.mp4"
    audio.write_bytes(b"mp3")
    video.write_bytes(b"mp4")

    files = collect_insert_files([audio, video], _config())

    assert files == [audio, video]


def test_collect_insert_files_accepts_code_and_html(tmp_path: Path) -> None:
    code = tmp_path / "module.py"
    html = tmp_path / "index.html"
    code.write_text("def run():\n    pass\n", encoding="utf-8")
    html.write_text("<main></main>", encoding="utf-8")

    files = collect_insert_files([tmp_path], _config())

    assert files == [html, code]


def test_collect_directory_skips_workspace(tmp_path: Path) -> None:
    source = tmp_path / "a.md"
    workspace_file = tmp_path / "workspace" / "kb" / "wiki" / "pages" / "old.md"
    source.write_text("# A", encoding="utf-8")
    workspace_file.parent.mkdir(parents=True)
    workspace_file.write_text("# Old", encoding="utf-8")

    files = collect_insert_files([tmp_path], _config())

    assert files == [source]


def test_normalize_wiki_pages_assigns_max_plus_one_without_reusing_deleted_ids(tmp_path: Path) -> None:
    wiki = tmp_path / "wiki"
    pages = wiki / "pages"
    pages.mkdir(parents=True)
    (wiki / "index.md").write_text("# Wiki Index\n\n", encoding="utf-8")
    (pages / "1-old.md").write_text(
        frontmatter_page("Old", "old.md", "Old summary.", "Old content."),
        encoding="utf-8",
    )
    (pages / "3-existing.md").write_text(
        frontmatter_page("Existing", "existing.md", "Existing summary.", "Existing content."),
        encoding="utf-8",
    )
    (pages / "new-topic.md").write_text(
        frontmatter_page("New Topic", "new.md", "New summary.", "New content."),
        encoding="utf-8",
    )

    result = normalize_wiki_pages(wiki)

    assert result.path_map == {"pages/new-topic.md": "pages/4-new-topic.md"}
    assert (pages / "4-new-topic.md").exists()
    index = (wiki / "index.md").read_text(encoding="utf-8")
    assert "- [1] [[Old]] (pages/1-old.md) — Old summary." in index
    assert "- [3] [[Existing]] (pages/3-existing.md) — Existing summary." in index
    assert "- [4] [[New Topic]] (pages/4-new-topic.md) — New summary." in index


def test_repair_broken_wiki_links_downgrades_missing_targets(tmp_path: Path) -> None:
    wiki = tmp_path / "wiki"
    pages = wiki / "pages"
    pages.mkdir(parents=True)
    page = pages / "1-topic.md"
    page.write_text(
        frontmatter_page(
            "Topic",
            "source.md",
            "Summary.",
            "See [[Existing Topic]] and [[Missing Topic]].",
        ),
        encoding="utf-8",
    )
    (pages / "2-existing-topic.md").write_text(
        frontmatter_page("Existing Topic", "source.md", "Summary.", "Body."),
        encoding="utf-8",
    )

    repair_broken_wiki_links(wiki)

    text = page.read_text(encoding="utf-8")
    assert "[[Existing Topic]]" in text
    assert "[[Missing Topic]]" not in text
    assert "Missing Topic" in text
