"""capture.py 规则引擎测试"""

from __future__ import annotations

import json

from index1.cognitive.capture import (
    extract, _extract_symbols, _parse_conventional_commit, extract_fix_then_pass,
    extract_task_from_prompt,
)


class TestExtract:
    """extract() 主入口测试"""

    def test_unsupported_tool_returns_empty(self):
        result = extract("Read", {}, "output", "proj", "s1")
        assert result == []

    def test_none_tool(self):
        result = extract("UnknownTool", {}, "", "proj", "s1")
        assert result == []

    def test_write_tool(self):
        facts = extract(
            "Write",
            {"file_path": "/src/main.py"},
            "def hello():\n    pass",
            "proj", "s1",
        )
        assert len(facts) >= 1
        assert any("Modified /src/main.py" in f["assertion"] for f in facts)

    def test_edit_tool(self):
        facts = extract(
            "Edit",
            {"file_path": "/src/config.py"},
            "class Config:\n    pass",
            "proj", "s1",
        )
        assert len(facts) >= 1
        assert facts[0]["collection"] == "proj"
        assert facts[0]["session_id"] == "s1"

    def test_multiedit_tool(self):
        facts = extract(
            "MultiEdit",
            {"file_path": "/src/app.rs"},
            "pub fn main() {}",
            "proj", "s1",
        )
        assert len(facts) >= 1


class TestFileEdit:
    """_capture_file_edit 测试"""

    def test_no_file_path(self):
        facts = extract("Write", {}, "content", "proj", "s1")
        assert facts == []

    def test_empty_file_path(self):
        facts = extract("Write", {"file_path": ""}, "content", "proj", "s1")
        assert facts == []

    def test_extracts_symbols_from_output(self):
        """#264: 符号合并到主 fact 的 scope_entities，不再单独生成 fact"""
        output = """
def authenticate_user(username, password):
    pass

class UserManager:
    pass
"""
        facts = extract("Write", {"file_path": "/app.py"}, output, "proj", "s1")
        # 只生成 1 个主 fact（不再是 1+N 个）
        assert len(facts) == 1
        # 符号信息在 scope_entities 中
        import json
        entities = json.loads(facts[0]["scope_entities"])
        assert "authenticate_user" in entities
        assert "UserManager" in entities

    def test_scope_files_set(self):
        facts = extract("Edit", {"file_path": "/a.py"}, "x = 1", "proj", "s1")
        import json
        files = json.loads(facts[0]["scope_files"])
        assert "/a.py" in files

    def test_fact_structure(self):
        facts = extract("Write", {"file_path": "/a.py"}, "", "proj", "s1")
        f = facts[0]
        assert f["category"] == "code"
        assert f["source"] == "hook"
        assert f["status"] == "active"
        assert f["confidence"] == 1.0


class TestBash:
    """_capture_bash 测试"""

    def test_no_command(self):
        facts = extract("Bash", {}, "", "proj", "s1")
        assert facts == []

    def test_empty_command(self):
        facts = extract("Bash", {"command": ""}, "", "proj", "s1")
        assert facts == []

    def test_pip_install(self):
        facts = extract(
            "Bash",
            {"command": "pip install requests"},
            "Successfully installed requests-2.28.0",
            "proj", "s1",
        )
        assert any("requests" in f["assertion"] for f in facts)
        assert any(f["category"] == "config" for f in facts)

    def test_npm_install(self):
        facts = extract(
            "Bash",
            {"command": "npm install lodash"},
            "added 1 package",
            "proj", "s1",
        )
        assert any("lodash" in f["assertion"] for f in facts)

    def test_cargo_add(self):
        facts = extract(
            "Bash",
            {"command": "cargo add serde"},
            "Adding serde v1.0",
            "proj", "s1",
        )
        assert any("serde" in f["assertion"] for f in facts)

    def test_pytest_passed(self):
        facts = extract(
            "Bash",
            {"command": "pytest tests/ -x -q"},
            "15 passed in 2.3s",
            "proj", "s1",
        )
        assert any("passed" in f["assertion"] for f in facts)

    def test_pytest_failed(self):
        facts = extract(
            "Bash",
            {"command": "python -m pytest tests/"},
            "3 failed, 10 passed",
            "proj", "s1",
        )
        assert any("failed" in f["assertion"] for f in facts)

    def test_jest_test(self):
        facts = extract(
            "Bash",
            {"command": "jest --ci"},
            "Tests: 5 passed",
            "proj", "s1",
        )
        assert any("passed" in f["assertion"] for f in facts)

    def test_cargo_test(self):
        facts = extract(
            "Bash",
            {"command": "cargo test"},
            "test result: ok. 10 passed; 0 failed",
            "proj", "s1",
        )
        assert any("passed" in f["assertion"] for f in facts)

    def test_migration(self):
        facts = extract(
            "Bash",
            {"command": "alembic upgrade head"},
            "Running upgrade...",
            "proj", "s1",
        )
        assert any("migration" in f["assertion"].lower() for f in facts)

    def test_git_commit(self):
        facts = extract(
            "Bash",
            {"command": 'git commit -m "fix: resolve auth bug"'},
            "[main abc123] fix: resolve auth bug",
            "proj", "s1",
        )
        assert any("Committed" in f["assertion"] for f in facts)
        assert any("resolve auth bug" in f["assertion"] for f in facts)

    def test_no_test_output_match(self):
        """Test command without pass/fail indicators should not generate test facts"""
        facts = extract(
            "Bash",
            {"command": "pytest tests/"},
            "collecting...",  # no passed/failed
            "proj", "s1",
        )
        assert not any("Tests" in f["assertion"] for f in facts)

    def test_multiple_facts_from_one_command(self):
        """pip install + test in one command"""
        facts = extract(
            "Bash",
            {"command": "pip install pytest && pytest tests/"},
            "installed pytest\n5 passed",
            "proj", "s1",
        )
        assert len(facts) >= 2


class TestExtractSymbols:
    """_extract_symbols 正则提取测试"""

    def test_python_function(self):
        symbols = _extract_symbols("def fetch_data():", "/a.py")
        assert ("function", "fetch_data") in symbols

    def test_python_async_function(self):
        symbols = _extract_symbols("async def run_server():", "/a.py")
        assert ("function", "run_server") in symbols

    def test_python_class(self):
        symbols = _extract_symbols("class MyService:", "/a.py")
        assert ("class", "MyService") in symbols

    def test_python_skip_names(self):
        symbols = _extract_symbols("def self(): pass\ndef __init__(): pass", "/a.py")
        names = {s[1] for s in symbols}
        assert "self" not in names
        assert "__init__" not in names

    def test_rust_function(self):
        symbols = _extract_symbols("pub fn connect(url: &str) {}", "/a.rs")
        assert ("function", "connect") in symbols

    def test_rust_struct(self):
        symbols = _extract_symbols("pub struct Config {}", "/a.rs")
        assert ("struct", "Config") in symbols

    def test_rust_enum(self):
        symbols = _extract_symbols("enum Color { Red, Blue }", "/a.rs")
        assert ("enum", "Color") in symbols

    def test_rust_impl(self):
        symbols = _extract_symbols("impl Database {}", "/a.rs")
        assert ("impl", "Database") in symbols

    def test_js_function(self):
        symbols = _extract_symbols("function handleClick() {}", "/a.js")
        assert ("function", "handleClick") in symbols

    def test_js_export_function(self):
        symbols = _extract_symbols("export function fetchData() {}", "/a.js")
        assert ("function", "fetchData") in symbols

    def test_js_class(self):
        symbols = _extract_symbols("class EventEmitter {}", "/a.js")
        assert ("class", "EventEmitter") in symbols

    def test_ts_interface(self):
        symbols = _extract_symbols("interface UserProps {}", "/a.ts")
        assert ("interface", "UserProps") in symbols

    def test_ts_type(self):
        symbols = _extract_symbols("type Config = {}", "/a.ts")
        assert ("type", "Config") in symbols

    def test_go_function(self):
        symbols = _extract_symbols("func HandleRequest() {}", "/a.go")
        assert ("function", "HandleRequest") in symbols

    def test_go_method(self):
        symbols = _extract_symbols("func (s *Server) Start() {}", "/a.go")
        assert ("function", "Start") in symbols

    def test_go_struct(self):
        symbols = _extract_symbols("type Config struct {}", "/a.go")
        assert ("struct", "Config") in symbols

    def test_unknown_extension(self):
        symbols = _extract_symbols("anything here", "/a.unknown")
        assert symbols == []

    def test_dedup_symbols(self):
        symbols = _extract_symbols(
            "def foo():\n    pass\ndef foo():\n    pass", "/a.py"
        )
        foo_count = sum(1 for s in symbols if s[1] == "foo")
        assert foo_count == 1


# ── Conventional Commit 解析 ──


class TestConventionalCommit:
    def test_fix_commit_is_decision(self):
        """fix: commit → category='decision'"""
        facts = extract(
            "Bash",
            {"command": 'git commit -m "fix: resolve session timeout"'},
            "[main abc] fix: resolve session timeout",
            "proj", "s1",
        )
        commit_facts = [f for f in facts if "Committed" in f["assertion"]]
        assert len(commit_facts) == 1
        assert commit_facts[0]["category"] == "decision"

    def test_feat_commit_is_code(self):
        """feat: commit → category 仍为 'code'（feat 不是决策）"""
        facts = extract(
            "Bash",
            {"command": 'git commit -m "feat: add user profile page"'},
            "[main def] feat: add user profile page",
            "proj", "s1",
        )
        commit_facts = [f for f in facts if "Committed" in f["assertion"]]
        assert len(commit_facts) == 1
        assert commit_facts[0]["category"] == "code"

    def test_refactor_commit_is_decision(self):
        """refactor: commit → category='decision'"""
        fact = _parse_conventional_commit(
            "refactor(auth): split login module", "proj", "s1",
        )
        assert fact is not None
        assert fact["category"] == "decision"
        ctx = json.loads(fact["context_json"])
        assert ctx["commit_type"] == "refactor"
        assert ctx["scope"] == "auth"

    def test_context_json_in_commit(self):
        """conventional commit 产出 context_json"""
        fact = _parse_conventional_commit(
            "fix(db): resolve connection pooling", "proj", "s1",
        )
        assert fact is not None
        ctx = json.loads(fact["context_json"])
        assert ctx["commit_type"] == "fix"
        assert ctx["scope"] == "db"

    def test_non_conventional_returns_none(self):
        """非 conventional commit 格式返回 None"""
        assert _parse_conventional_commit("update readme", "proj", "s1") is None


class TestRevertDetection:
    def test_revert_commit(self):
        """revert: commit → category='decision'"""
        facts = extract(
            "Bash",
            {"command": 'git commit -m "revert: roll back SQLAlchemy 2.0"'},
            "[main 123] revert",
            "proj", "s1",
        )
        commit_facts = [f for f in facts if "Committed" in f["assertion"]]
        assert len(commit_facts) == 1
        assert commit_facts[0]["category"] == "decision"

    def test_git_revert_command(self):
        """git revert HEAD → category='decision'"""
        facts = extract(
            "Bash",
            {"command": "git revert HEAD"},
            "Revert \"fix: something\"",
            "proj", "s1",
        )
        revert_facts = [f for f in facts if "Reverted" in f["assertion"]]
        assert len(revert_facts) == 1
        assert revert_facts[0]["category"] == "decision"


class TestFixThenPass:
    def test_detects_pattern(self):
        """测试失败 → 修改文件 → 测试通过 → 产出 root_cause fact"""
        events = [
            {"type": "test_failed", "detail": "Tests failed: pytest tests/"},
            {"type": "file_modified", "detail": "/src/auth.py"},
            {"type": "test_passed", "detail": "Tests passed: pytest tests/"},
        ]
        facts = extract_fix_then_pass(events, "proj", "s1")
        assert len(facts) == 1
        assert facts[0]["category"] == "root_cause"
        assert "auth.py" in facts[0]["assertion"]

    def test_ignores_short_sequence(self):
        """事件不足 3 个时不触发"""
        events = [
            {"type": "test_failed", "detail": "fail"},
            {"type": "test_passed", "detail": "pass"},
        ]
        facts = extract_fix_then_pass(events, "proj", "s1")
        assert facts == []

    def test_no_failure_no_detection(self):
        """没有测试失败就没有 root_cause"""
        events = [
            {"type": "file_modified", "detail": "/a.py"},
            {"type": "file_modified", "detail": "/b.py"},
            {"type": "test_passed", "detail": "pass"},
        ]
        facts = extract_fix_then_pass(events, "proj", "s1")
        assert facts == []


# ── Task 提取 ──


class TestExtractTaskFromPrompt:
    def test_fix_issue_number(self):
        """fix #80 → 提取任务"""
        task = extract_task_from_prompt("Please fix #80 task context tracking")
        assert "80" in task

    def test_chinese_fix(self):
        """修复 Issue 68 → 提取任务"""
        task = extract_task_from_prompt("修复 Issue 68 watcher 崩溃恢复")
        assert "68" in task

    def test_implement(self):
        """implement user auth → 提取任务"""
        task = extract_task_from_prompt("implement user authentication for the app")
        assert "user authentication" in task.lower()

    def test_add_feature(self):
        """添加 dark mode → 提取任务"""
        task = extract_task_from_prompt("添加 dark mode 支持")
        assert "dark mode" in task.lower()

    def test_bare_issue_ref(self):
        """#123 作为独立引用"""
        task = extract_task_from_prompt("Look at #123 and fix the bug")
        assert "123" in task

    def test_no_match(self):
        """普通问题不匹配"""
        task = extract_task_from_prompt("How does the search module work?")
        assert task == ""

    def test_empty_prompt(self):
        """空 prompt 返回空"""
        assert extract_task_from_prompt("") == ""

    def test_long_prompt_truncated(self):
        """超长 prompt 只取前 200 字符"""
        long_text = "fix #42 " + "x" * 300
        task = extract_task_from_prompt(long_text)
        assert "42" in task
        assert len(task) <= 100
