"""Tests for aura core functionality."""

import json
import tempfile
from pathlib import Path

import pytest

from aura.schema import ContextPack, Fact, Rule, PackMeta, FactType, Confidence
from aura.exporters.system_prompt import export_system_prompt
from aura.exporters.cursorrules import export_cursorrules
from aura.exporters.claude_memory import export_claude_memory, export_claude_memory_text
from aura.exporters.chatgpt_instructions import export_chatgpt_instructions
from aura.importers.chatgpt import import_chatgpt_export


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def dev_pack() -> ContextPack:
    return ContextPack(
        name="developer",
        scope="development",
        facts=[
            Fact(key="languages.primary", value=["Python", "TypeScript"], type=FactType.SKILL),
            Fact(key="frameworks", value=["Next.js", "FastAPI"], type=FactType.SKILL),
            Fact(key="style.comments", value="Minimal", type=FactType.STYLE),
        ],
        rules=[
            Rule(instruction="Always use TypeScript strict mode", priority=8),
            Rule(instruction="Prefer functional patterns", priority=5),
        ],
        meta=PackMeta(description="Test dev pack"),
    )


@pytest.fixture
def writer_pack() -> ContextPack:
    return ContextPack(
        name="writer",
        scope="writing",
        facts=[
            Fact(key="tone", value="Direct, no fluff", type=FactType.STYLE),
            Fact(key="audience", value="Technical professionals", type=FactType.CONTEXT),
        ],
        rules=[
            Rule(instruction="Use active voice", priority=6),
        ],
        meta=PackMeta(description="Test writer pack"),
    )


@pytest.fixture
def mock_chatgpt_export(tmp_path) -> Path:
    conversations = [
        {
            "title": "Python help",
            "mapping": {
                "m1": {
                    "message": {
                        "author": {"role": "user"},
                        "content": {"parts": ["I use Python and TypeScript daily. Help me with FastAPI."]}
                    }
                },
                "m2": {
                    "message": {
                        "author": {"role": "user"},
                        "content": {"parts": ["I'm a software engineer at Acme Corp."]}
                    }
                },
            }
        }
    ]
    path = tmp_path / "conversations.json"
    path.write_text(json.dumps(conversations))
    return path


# ---------------------------------------------------------------------------
# Schema tests
# ---------------------------------------------------------------------------
class TestContextPack:
    def test_create_pack(self, dev_pack):
        assert dev_pack.name == "developer"
        assert dev_pack.scope == "development"
        assert len(dev_pack.facts) == 3
        assert len(dev_pack.rules) == 2

    def test_to_system_prompt(self, dev_pack):
        prompt = dev_pack.to_system_prompt()
        assert "developer" in prompt
        assert "Python, TypeScript" in prompt
        assert "TypeScript strict mode" in prompt

    def test_to_cursorrules(self, dev_pack):
        rules = dev_pack.to_cursorrules()
        assert "# Aura Context: developer" in rules
        assert "languages.primary" in rules
        assert "TypeScript strict mode" in rules

    def test_name_validation(self):
        # Valid names
        ContextPack(name="dev", scope="development")
        ContextPack(name="my-pack", scope="general")
        ContextPack(name="pack_2", scope="general")

        # Invalid names should fail
        with pytest.raises(Exception):
            ContextPack(name="UPPERCASE", scope="general")
        with pytest.raises(Exception):
            ContextPack(name="has space", scope="general")


# ---------------------------------------------------------------------------
# Exporter tests
# ---------------------------------------------------------------------------
class TestSystemPromptExporter:
    def test_single_pack(self, dev_pack):
        result = export_system_prompt([dev_pack])
        assert "# User Context" in result
        assert "developer" in result
        assert "Python, TypeScript" in result

    def test_multi_pack(self, dev_pack, writer_pack):
        result = export_system_prompt([dev_pack, writer_pack])
        assert "developer" in result
        assert "writer" in result
        assert "Direct, no fluff" in result

    def test_no_header(self, dev_pack):
        result = export_system_prompt([dev_pack], include_header=False)
        assert "# User Context" not in result
        assert "developer" in result


class TestCursorrulesExporter:
    def test_export(self, dev_pack):
        result = export_cursorrules([dev_pack])
        assert "Generated by aura" in result
        assert "languages.primary" in result

    def test_rules_included(self, dev_pack):
        result = export_cursorrules([dev_pack])
        assert "TypeScript strict mode" in result


class TestClaudeMemoryExporter:
    def test_export_list(self, dev_pack):
        statements = export_claude_memory([dev_pack])
        assert len(statements) > 0
        assert any("Python, TypeScript" in s for s in statements)

    def test_export_text(self, dev_pack):
        text = export_claude_memory_text([dev_pack])
        assert "Claude Memory Export" in text
        assert "Total statements:" in text


class TestChatGPTInstructionsExporter:
    def test_export_structure(self, dev_pack, writer_pack):
        result = export_chatgpt_instructions([dev_pack, writer_pack])
        assert "about_you" in result
        assert "response_style" in result
        assert "DEVELOPMENT" in result["about_you"]
        assert "WRITING" in result["about_you"]


# ---------------------------------------------------------------------------
# Importer tests
# ---------------------------------------------------------------------------
class TestChatGPTImporter:
    def test_import_json(self, mock_chatgpt_export):
        pack = import_chatgpt_export(mock_chatgpt_export)
        assert pack.name == "chatgpt-import"
        assert len(pack.facts) > 0
        assert "chatgpt" in pack.meta.tags

    def test_import_detects_languages(self, mock_chatgpt_export):
        pack = import_chatgpt_export(mock_chatgpt_export)
        lang_facts = [f for f in pack.facts if "language" in f.key]
        assert len(lang_facts) > 0

    def test_import_custom_name(self, mock_chatgpt_export):
        pack = import_chatgpt_export(mock_chatgpt_export, pack_name="my-import", scope="dev")
        assert pack.name == "my-import"
        assert pack.scope == "dev"

    def test_import_nonexistent_file(self):
        with pytest.raises(FileNotFoundError):
            import_chatgpt_export("/nonexistent/path.json")

    def test_import_wrong_format(self, tmp_path):
        bad_file = tmp_path / "bad.txt"
        bad_file.write_text("not json")
        with pytest.raises(ValueError):
            import_chatgpt_export(bad_file)


# ---------------------------------------------------------------------------
# Pack manager tests
# ---------------------------------------------------------------------------
class TestPackManager:
    def test_init_and_save(self, tmp_path, monkeypatch, dev_pack):
        from aura import pack as pack_module
        monkeypatch.setattr(pack_module, "get_aura_home", lambda: tmp_path / ".aura")
        monkeypatch.setattr(pack_module, "get_packs_dir", lambda: tmp_path / ".aura" / "packs")

        pack_module.init_aura()
        assert (tmp_path / ".aura" / "packs").exists()

        path = pack_module.save_pack(dev_pack)
        assert Path(path).exists()

        loaded = pack_module.load_pack("developer")
        assert loaded.name == "developer"
        assert len(loaded.facts) == 3
        assert len(loaded.rules) == 2

    def test_list_packs(self, tmp_path, monkeypatch, dev_pack, writer_pack):
        from aura import pack as pack_module
        monkeypatch.setattr(pack_module, "get_aura_home", lambda: tmp_path / ".aura")
        monkeypatch.setattr(pack_module, "get_packs_dir", lambda: tmp_path / ".aura" / "packs")

        pack_module.init_aura()
        pack_module.save_pack(dev_pack)
        pack_module.save_pack(writer_pack)

        packs = pack_module.list_packs()
        assert len(packs) == 2
        names = {p.name for p in packs}
        assert "developer" in names
        assert "writer" in names

    def test_delete_pack(self, tmp_path, monkeypatch, dev_pack):
        from aura import pack as pack_module
        monkeypatch.setattr(pack_module, "get_aura_home", lambda: tmp_path / ".aura")
        monkeypatch.setattr(pack_module, "get_packs_dir", lambda: tmp_path / ".aura" / "packs")

        pack_module.init_aura()
        pack_module.save_pack(dev_pack)
        assert pack_module.pack_exists("developer")

        pack_module.delete_pack("developer")
        assert not pack_module.pack_exists("developer")

    def test_create_from_template(self):
        from aura.pack import create_from_template

        pack = create_from_template("developer")
        assert pack.name == "developer"
        assert pack.scope == "development"
        assert len(pack.facts) > 0
        assert len(pack.rules) > 0

    def test_create_from_unknown_template(self):
        from aura.pack import create_from_template

        with pytest.raises(ValueError, match="Unknown template"):
            create_from_template("nonexistent")


# ---------------------------------------------------------------------------
# Diff tests
# ---------------------------------------------------------------------------
class TestDiff:
    def test_identical_packs(self, dev_pack):
        from aura.diff import diff_packs
        result = diff_packs(dev_pack, dev_pack)
        assert not result.has_differences
        assert len(result.matching) == len(dev_pack.facts)

    def test_different_packs(self, dev_pack, writer_pack):
        from aura.diff import diff_packs
        result = diff_packs(dev_pack, writer_pack)
        assert result.has_differences
        assert len(result.only_in_source) > 0
        assert len(result.only_in_target) > 0

    def test_conflicting_facts(self):
        from aura.diff import diff_packs
        pack_a = ContextPack(
            name="a", scope="test",
            facts=[Fact(key="language", value="Python")],
        )
        pack_b = ContextPack(
            name="b", scope="test",
            facts=[Fact(key="language", value="Rust")],
        )
        result = diff_packs(pack_a, pack_b)
        assert len(result.conflicts) == 1
        assert result.conflicts[0][0].value == "Python"
        assert result.conflicts[0][1].value == "Rust"

    def test_format_diff(self, dev_pack, writer_pack):
        from aura.diff import diff_packs, format_diff
        result = diff_packs(dev_pack, writer_pack)
        text = format_diff(result, "local", "platform")
        assert "local" in text
        assert "platform" in text

    def test_summary(self):
        from aura.diff import DiffResult
        result = DiffResult(
            matching=[Fact(key="a", value="1")],
            only_in_source=[Fact(key="b", value="2")],
        )
        assert "1 matching" in result.summary
        assert "1 only in source" in result.summary


# ---------------------------------------------------------------------------
# MCP Server tests
# ---------------------------------------------------------------------------
class TestMCPServer:
    def test_initialize(self):
        from aura.mcp_server import handle_jsonrpc
        r = handle_jsonrpc({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}})
        assert r["result"]["protocolVersion"]
        assert r["result"]["serverInfo"]["name"] == "aura"

    def test_resources_list(self):
        from aura.mcp_server import handle_jsonrpc
        r = handle_jsonrpc({"jsonrpc": "2.0", "id": 2, "method": "resources/list", "params": {}})
        resources = r["result"]["resources"]
        assert len(resources) > 0
        uris = [res["uri"] for res in resources]
        assert "aura://context/full" in uris

    def test_tools_list(self):
        from aura.mcp_server import handle_jsonrpc
        r = handle_jsonrpc({"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}})
        tools = r["result"]["tools"]
        tool_names = [t["name"] for t in tools]
        assert "search_context" in tool_names
        assert "get_context" in tool_names
        assert "get_all_context" in tool_names

    def test_search_context(self):
        from aura.mcp_server import handle_jsonrpc
        r = handle_jsonrpc({
            "jsonrpc": "2.0", "id": 4,
            "method": "tools/call",
            "params": {"name": "search_context", "arguments": {"query": "typescript"}}
        })
        text = r["result"]["content"][0]["text"]
        assert "TypeScript" in text or "No context found" in text

    def test_unknown_method(self):
        from aura.mcp_server import handle_jsonrpc
        r = handle_jsonrpc({"jsonrpc": "2.0", "id": 99, "method": "fake/method", "params": {}})
        assert "error" in r
        assert r["error"]["code"] == -32601

    def test_ping(self):
        from aura.mcp_server import handle_jsonrpc
        r = handle_jsonrpc({"jsonrpc": "2.0", "id": 5, "method": "ping", "params": {}})
        assert r["result"] == {}


# ---------------------------------------------------------------------------
# Extractor tests
# ---------------------------------------------------------------------------
class TestExtractor:
    def test_parse_valid_response(self):
        from aura.extractor import Extractor
        response = '{"facts": [{"key": "role", "value": "engineer", "type": "identity", "confidence": "high"}], "rules": [{"instruction": "Use TypeScript", "priority": 8}]}'
        result = Extractor._parse_response(response)
        assert result is not None
        assert len(result["facts"]) == 1
        assert result["facts"][0]["key"] == "role"
        assert len(result["rules"]) == 1

    def test_parse_with_markdown_fences(self):
        from aura.extractor import Extractor
        response = '```json\n{"facts": [{"key": "lang", "value": "Python"}], "rules": []}\n```'
        result = Extractor._parse_response(response)
        assert result is not None
        assert len(result["facts"]) == 1

    def test_parse_invalid_response(self):
        from aura.extractor import Extractor
        result = Extractor._parse_response("this is not json at all")
        assert result is None

    def test_merge_facts(self):
        from aura.extractor import Extractor
        facts = [
            {"key": "lang", "value": "Python", "confidence": "low"},
            {"key": "lang", "value": "Python", "confidence": "high"},
            {"key": "role", "value": "engineer", "confidence": "high"},
        ]
        merged = Extractor._merge_facts(facts)
        assert len(merged) == 2
        lang_fact = [f for f in merged if f["key"] == "lang"][0]
        assert lang_fact["confidence"] == "high"

    def test_merge_facts_list_values(self):
        from aura.extractor import Extractor
        facts = [
            {"key": "langs", "value": ["Python", "JS"], "confidence": "medium"},
            {"key": "langs", "value": ["JS", "Rust"], "confidence": "medium"},
        ]
        merged = Extractor._merge_facts(facts)
        assert len(merged) == 1
        assert set(merged[0]["value"]) == {"Python", "JS", "Rust"}

    def test_merge_rules_dedup(self):
        from aura.extractor import Extractor
        rules = [
            {"instruction": "Use TypeScript strict mode", "priority": 8},
            {"instruction": "Use TypeScript strict mode!", "priority": 7},
            {"instruction": "Write tests for all functions", "priority": 5},
        ]
        merged = Extractor._merge_rules(rules)
        assert len(merged) == 2


# ---------------------------------------------------------------------------
# Setup tests
# ---------------------------------------------------------------------------
class TestSetup:
    def test_detect_tools(self):
        from aura.setup import detect_installed_tools
        tools = detect_installed_tools()
        assert isinstance(tools, list)
        for tool in tools:
            assert "name" in tool
            assert "installed" in tool
            assert "config_path" in tool

    def test_aura_mcp_config(self):
        from aura.setup import _aura_mcp_config
        config = _aura_mcp_config("localhost", 3847)
        assert config["url"] == "http://localhost:3847/mcp"

    def test_aura_mcp_config_custom(self):
        from aura.setup import _aura_mcp_config
        config = _aura_mcp_config("0.0.0.0", 9000)
        assert config["url"] == "http://0.0.0.0:9000/mcp"

    def test_setup_claude_creates_config(self, tmp_path, monkeypatch):
        from aura import setup as setup_module
        config_path = tmp_path / "Claude" / "claude_desktop_config.json"
        monkeypatch.setattr(setup_module, "_get_claude_config_path", lambda: config_path)

        result = setup_module.setup_claude_desktop()
        assert result["success"]
        assert result["action"] == "configured"
        assert config_path.exists()

        # Verify JSON content
        with open(config_path) as f:
            data = json.load(f)
        assert "aura" in data["mcpServers"]
        assert "localhost:3847" in data["mcpServers"]["aura"]["url"]

    def test_setup_claude_already_configured(self, tmp_path, monkeypatch):
        from aura import setup as setup_module
        config_path = tmp_path / "Claude" / "claude_desktop_config.json"
        config_path.parent.mkdir(parents=True)
        config_path.write_text(json.dumps({"mcpServers": {"aura": {"url": "http://localhost:3847/mcp"}}}))
        monkeypatch.setattr(setup_module, "_get_claude_config_path", lambda: config_path)

        result = setup_module.setup_claude_desktop()
        assert result["success"]
        assert result["action"] == "already_configured"

    def test_setup_cursor_creates_config(self, tmp_path, monkeypatch):
        from aura import setup as setup_module
        config_path = tmp_path / ".cursor" / "mcp.json"
        monkeypatch.setattr(setup_module, "_get_cursor_config_path", lambda: config_path)

        result = setup_module.setup_cursor()
        assert result["success"]
        assert result["action"] == "configured"
        assert config_path.exists()

    def test_setup_preserves_existing_servers(self, tmp_path, monkeypatch):
        from aura import setup as setup_module
        config_path = tmp_path / "Claude" / "claude_desktop_config.json"
        config_path.parent.mkdir(parents=True)
        config_path.write_text(json.dumps({
            "mcpServers": {"other-server": {"url": "http://localhost:5000"}}
        }))
        monkeypatch.setattr(setup_module, "_get_claude_config_path", lambda: config_path)

        result = setup_module.setup_claude_desktop()
        assert result["success"]

        with open(config_path) as f:
            data = json.load(f)
        assert "other-server" in data["mcpServers"]
        assert "aura" in data["mcpServers"]


# ---------------------------------------------------------------------------
# Scanner tests
# ---------------------------------------------------------------------------
class TestScanner:
    def test_scan_returns_pack(self, tmp_path):
        from aura.scanner import Scanner
        scanner = Scanner(scan_dirs=[str(tmp_path)])
        pack = scanner.scan()
        assert pack.name == "scanned"
        assert pack.scope == "development"
        assert "scanned" in pack.meta.tags

    def test_scan_detects_repo_languages(self, tmp_path):
        from aura.scanner import Scanner
        # Create a fake repo with Python and TypeScript files
        repo = tmp_path / "my-project" / ".git"
        repo.mkdir(parents=True)
        (tmp_path / "my-project" / "main.py").write_text("print('hello')")
        (tmp_path / "my-project" / "app.ts").write_text("console.log('hi')")
        (tmp_path / "my-project" / "util.ts").write_text("export const x = 1")

        scanner = Scanner(scan_dirs=[str(tmp_path)])
        pack = scanner.scan()

        lang_facts = [f for f in pack.facts if f.key == "languages.primary"]
        assert len(lang_facts) == 1
        assert "Python" in lang_facts[0].value
        assert "TypeScript" in lang_facts[0].value

    def test_scan_detects_frameworks(self, tmp_path):
        from aura.scanner import Scanner
        repo = tmp_path / "nextjs-app" / ".git"
        repo.mkdir(parents=True)
        (tmp_path / "nextjs-app" / "package.json").write_text(json.dumps({
            "dependencies": {"next": "15.0.0", "react": "19.0.0", "tailwindcss": "4.0.0"}
        }))

        scanner = Scanner(scan_dirs=[str(tmp_path)])
        pack = scanner.scan()

        fw_facts = [f for f in pack.facts if f.key == "frameworks"]
        assert len(fw_facts) == 1
        assert "Next.js" in fw_facts[0].value
        assert "React" in fw_facts[0].value
        assert "Tailwind CSS" in fw_facts[0].value

    def test_scan_detects_python_frameworks(self, tmp_path):
        from aura.scanner import Scanner
        repo = tmp_path / "api" / ".git"
        repo.mkdir(parents=True)
        (tmp_path / "api" / "requirements.txt").write_text("fastapi==0.104\npandas==2.0\npydantic==2.0\n")
        (tmp_path / "api" / "main.py").write_text("from fastapi import FastAPI")

        scanner = Scanner(scan_dirs=[str(tmp_path)])
        pack = scanner.scan()

        fw_facts = [f for f in pack.facts if f.key == "frameworks"]
        assert len(fw_facts) == 1
        assert "FastAPI" in fw_facts[0].value

    def test_scan_detects_project_names(self, tmp_path):
        from aura.scanner import Scanner
        for name in ["project-a", "project-b", "aura"]:
            (tmp_path / name / ".git").mkdir(parents=True)
            (tmp_path / name / "main.py").write_text("")

        scanner = Scanner(scan_dirs=[str(tmp_path)])
        pack = scanner.scan()

        proj_facts = [f for f in pack.facts if f.key == "projects.recent"]
        assert len(proj_facts) == 1
        names = proj_facts[0].value
        assert "project-a" in names or "project-b" in names or "aura" in names

    def test_scan_system_info(self):
        from aura.scanner import Scanner
        scanner = Scanner(scan_dirs=[])
        pack = scanner.scan()

        os_facts = [f for f in pack.facts if f.key == "system.os"]
        assert len(os_facts) == 1
        assert os_facts[0].value  # Should have something

    def test_scan_empty_dir(self, tmp_path):
        from aura.scanner import Scanner
        scanner = Scanner(scan_dirs=[str(tmp_path)])
        pack = scanner.scan()
        # Should not crash, just have minimal facts
        assert isinstance(pack.facts, list)


# ---------------------------------------------------------------------------
# Onboard tests
# ---------------------------------------------------------------------------
class TestOnboard:
    def test_onboard_direct_tone(self):
        from aura.onboard import Onboarder
        answers = iter([
            "Full-stack developer",  # role
            "1",                      # tone = direct
            "Building aura",          # priorities
            "No corporate jargon, No emojis",  # rules
            "English and French",     # languages
        ])
        onboarder = Onboarder()
        packs = onboarder.run(ask_fn=lambda _: next(answers))

        assert "writer" in packs
        assert "work" in packs

        writer = packs["writer"]
        tone_facts = [f for f in writer.facts if f.key == "tone"]
        assert len(tone_facts) == 1
        assert "Direct" in tone_facts[0].value

        # Should have rules from the preset + custom rules
        assert len(writer.rules) >= 3

    def test_onboard_custom_tone(self):
        from aura.onboard import Onboarder
        answers = iter([
            "Designer",                      # role
            "Playful but precise",           # custom tone
            "Redesigning my portfolio",      # priorities
            "skip",                          # no rules
            "English",                       # languages
        ])
        onboarder = Onboarder()
        packs = onboarder.run(ask_fn=lambda _: next(answers))

        assert "writer" in packs
        tone_facts = [f for f in packs["writer"].facts if f.key == "tone"]
        assert tone_facts[0].value == "Playful but precise"

    def test_onboard_skip_all(self):
        from aura.onboard import Onboarder
        answers = iter(["skip", "skip", "skip", "skip", "skip"])
        onboarder = Onboarder()
        packs = onboarder.run(ask_fn=lambda _: next(answers))
        assert len(packs) == 0

    def test_onboard_work_pack_has_role(self):
        from aura.onboard import Onboarder
        answers = iter([
            "ML Engineer at DeepMind",
            "3",                        # technical tone
            "Training transformers",
            "skip",
            "English",
        ])
        onboarder = Onboarder()
        packs = onboarder.run(ask_fn=lambda _: next(answers))

        assert "work" in packs
        role_facts = [f for f in packs["work"].facts if f.key == "role"]
        assert "DeepMind" in role_facts[0].value

    def test_onboard_rules_parsing(self):
        from aura.onboard import Onboarder
        answers = iter([
            "Dev",
            "1",
            "Building stuff",
            "No jQuery, Always use strict mode, Prefer functional patterns",
            "English",
        ])
        onboarder = Onboarder()
        packs = onboarder.run(ask_fn=lambda _: next(answers))

        writer = packs["writer"]
        custom_rules = [r for r in writer.rules if "jQuery" in r.instruction
                       or "strict" in r.instruction
                       or "functional" in r.instruction]
        assert len(custom_rules) == 3


# ---------------------------------------------------------------------------
# Doctor tests
# ---------------------------------------------------------------------------
class TestDoctor:
    def test_healthy_packs(self, dev_pack, writer_pack):
        from aura.doctor import diagnose
        report = diagnose([dev_pack, writer_pack])
        assert report.score > 0
        assert isinstance(report.issues, list)

    def test_empty_pack_warning(self):
        from aura.doctor import diagnose
        empty = ContextPack(name="empty", scope="test", facts=[], rules=[])
        report = diagnose([empty])
        warnings = [i for i in report.issues if "empty" in i.message.lower()]
        assert len(warnings) > 0

    def test_no_packs_warning(self):
        from aura.doctor import diagnose
        report = diagnose([])
        assert report.score == 0
        assert len(report.issues) > 0

    def test_bloated_pack_warning(self):
        from aura.doctor import diagnose
        facts = [Fact(key=f"fact_{i}", value=f"value_{i}") for i in range(50)]
        bloated = ContextPack(name="bloated", scope="test", facts=facts)
        report = diagnose([bloated])
        size_warnings = [i for i in report.issues if "facts" in i.message.lower() and "recommended" in i.message.lower()]
        assert len(size_warnings) > 0

    def test_duplicate_across_packs(self):
        from aura.doctor import diagnose
        pack_a = ContextPack(
            name="a", scope="dev",
            facts=[Fact(key="lang", value="Python")],
        )
        pack_b = ContextPack(
            name="b", scope="work",
            facts=[Fact(key="lang", value="Python")],
        )
        report = diagnose([pack_a, pack_b])
        dup_issues = [i for i in report.issues if "duplicate" in i.message.lower()]
        assert len(dup_issues) > 0

    def test_no_rules_info(self, dev_pack):
        from aura.doctor import diagnose
        # dev_pack has rules, so create one without
        no_rules = ContextPack(
            name="norules", scope="test",
            facts=[Fact(key="x", value="y")],
            rules=[],
        )
        report = diagnose([no_rules])
        rule_issues = [i for i in report.issues if "no rules" in i.message.lower()]
        assert len(rule_issues) > 0

    def test_format_report(self, dev_pack):
        from aura.doctor import diagnose, format_report
        report = diagnose([dev_pack])
        text = format_report(report)
        assert "score" in text.lower() or "Health" in text


# ---------------------------------------------------------------------------
# Claude Importer Tests
# ---------------------------------------------------------------------------
class TestClaudeImporter:
    def test_import_chat_messages_format(self, tmp_path):
        """Test Claude export with chat_messages format."""
        from aura.importers.claude import import_claude_export

        data = [
            {
                "uuid": "abc-123",
                "name": "Test conversation",
                "chat_messages": [
                    {"sender": "human", "text": "I use Python and TypeScript for my projects"},
                    {"sender": "assistant", "text": "Great! Those are solid choices."},
                    {"sender": "human", "text": "I'm building a Next.js app with Supabase"},
                ],
            }
        ]

        json_path = tmp_path / "conversations.json"
        json_path.write_text(json.dumps(data))

        pack = import_claude_export(json_path)
        assert pack.name == "claude-import"
        assert len(pack.facts) > 0

        # Should detect languages
        lang_facts = [f for f in pack.facts if "languages" in f.key]
        assert len(lang_facts) > 0

    def test_import_messages_format(self, tmp_path):
        """Test generic messages format."""
        from aura.importers.claude import import_claude_export

        data = [
            {
                "messages": [
                    {"role": "user", "content": "Je travaille avec React et FastAPI"},
                    {"role": "assistant", "content": "OK"},
                    {"role": "user", "content": "Mon éditeur préféré est Cursor"},
                ],
            }
        ]

        json_path = tmp_path / "conversations.json"
        json_path.write_text(json.dumps(data))

        pack = import_claude_export(json_path)
        assert len(pack.facts) > 0

        # Should detect French
        style_facts = [f for f in pack.facts if "style.language" in f.key]
        assert len(style_facts) > 0

    def test_import_zip(self, tmp_path):
        """Test import from ZIP file."""
        import zipfile as zf
        from aura.importers.claude import import_claude_export

        data = [
            {
                "chat_messages": [
                    {"sender": "human", "text": "I code in Rust and Go"},
                ]
            }
        ]

        json_path = tmp_path / "conversations.json"
        json_path.write_text(json.dumps(data))

        zip_path = tmp_path / "export.zip"
        with zf.ZipFile(zip_path, "w") as z:
            z.write(json_path, "conversations.json")

        pack = import_claude_export(zip_path)
        assert len(pack.facts) > 0

    def test_import_empty(self, tmp_path):
        """Test import with no user messages."""
        from aura.importers.claude import import_claude_export

        data = [{"chat_messages": []}]
        json_path = tmp_path / "conversations.json"
        json_path.write_text(json.dumps(data))

        pack = import_claude_export(json_path)
        assert len(pack.facts) == 0


# ---------------------------------------------------------------------------
# Compact Profile Tests
# ---------------------------------------------------------------------------
class TestCompactProfile:
    def test_compact_profile_output(self):
        from aura.mcp_server import _compact_profile
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta

        pack = ContextPack(
            name="test",
            scope="developer",
            facts=[
                Fact(key="identity.name", value="Nok", type=FactType.IDENTITY, confidence=Confidence.HIGH),
                Fact(key="languages.primary", value=["Python", "TypeScript"], type=FactType.SKILL, confidence=Confidence.HIGH),
            ],
            meta=PackMeta(description="Test"),
        )

        result = _compact_profile([pack])
        assert "Nok" in result
        assert "Python" in result
        assert len(result) < 500  # Should be compact

    def test_compact_profile_max_facts(self):
        from aura.mcp_server import _compact_profile
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta

        facts = [
            Fact(key=f"pref.item{i}", value=f"value{i}", type=FactType.PREFERENCE, confidence=Confidence.LOW)
            for i in range(50)
        ]

        pack = ContextPack(
            name="test",
            scope="general",
            facts=facts,
            meta=PackMeta(description="Test"),
        )

        result_full = _compact_profile([pack])
        result_limited = _compact_profile([pack], max_facts=5)
        assert len(result_limited) < len(result_full)

    def test_compact_profile_empty(self):
        from aura.mcp_server import _compact_profile
        result = _compact_profile([])
        assert "No user context" in result


# ---------------------------------------------------------------------------
# Claude Importer
# ---------------------------------------------------------------------------
class TestClaudeImporter:
    def test_import_claude_chat_messages(self, tmp_path):
        """Test importing Claude export with chat_messages format."""
        from aura.importers.claude import import_claude_export

        data = [
            {
                "name": "Python help",
                "chat_messages": [
                    {"sender": "human", "text": "Help me write a Python script for web scraping"},
                    {"sender": "assistant", "text": "Sure, here's a script using BeautifulSoup..."},
                    {"sender": "human", "text": "Can you use TypeScript and Next.js instead?"},
                ],
            },
            {
                "name": "Rust question",
                "chat_messages": [
                    {"sender": "human", "text": "How do I use cargo to build a Rust project?"},
                ],
            },
        ]

        json_path = tmp_path / "conversations.json"
        json_path.write_text(json.dumps(data))

        pack = import_claude_export(json_path, pack_name="test-claude")
        assert pack.name == "test-claude"
        assert len(pack.facts) > 0

        # Should detect languages
        lang_facts = [f for f in pack.facts if "languages" in f.key]
        assert len(lang_facts) > 0
        langs = lang_facts[0].value
        assert "Python" in langs

    def test_import_claude_messages_format(self, tmp_path):
        """Test importing with standard messages array."""
        from aura.importers.claude import import_claude_export

        data = [
            {
                "messages": [
                    {"role": "user", "content": "I'm building a React dashboard with Supabase"},
                    {"role": "assistant", "content": "Great choice!"},
                ],
            },
        ]

        json_path = tmp_path / "claude.json"
        json_path.write_text(json.dumps(data))

        pack = import_claude_export(json_path)
        fw_facts = [f for f in pack.facts if "frameworks" in f.key]
        assert len(fw_facts) > 0
        assert "React" in fw_facts[0].value or "Supabase" in fw_facts[0].value

    def test_import_claude_zip(self, tmp_path):
        """Test importing from ZIP file."""
        import zipfile
        from aura.importers.claude import import_claude_export

        data = [
            {
                "chat_messages": [
                    {"sender": "human", "text": "I use Django and PostgreSQL for my backend"},
                ],
            },
        ]

        json_path = tmp_path / "conversations.json"
        json_path.write_text(json.dumps(data))

        zip_path = tmp_path / "export.zip"
        with zipfile.ZipFile(zip_path, "w") as zf:
            zf.write(json_path, "conversations.json")

        pack = import_claude_export(zip_path)
        assert len(pack.facts) > 0

    def test_import_claude_empty(self, tmp_path):
        """Test importing empty conversations."""
        from aura.importers.claude import import_claude_export

        json_path = tmp_path / "empty.json"
        json_path.write_text("[]")

        pack = import_claude_export(json_path)
        assert pack.name == "claude-import"
        assert len(pack.facts) == 0

    def test_import_claude_style_detection(self, tmp_path):
        """Test communication style detection."""
        from aura.importers.claude import import_claude_export

        # Lots of short messages
        data = [
            {
                "chat_messages": [
                    {"sender": "human", "text": "fix this"},
                    {"sender": "human", "text": "run it"},
                    {"sender": "human", "text": "show me"},
                    {"sender": "human", "text": "try again"},
                    {"sender": "human", "text": "that works"},
                ] * 5,
            },
        ]

        json_path = tmp_path / "style.json"
        json_path.write_text(json.dumps(data))

        pack = import_claude_export(json_path)
        style_facts = [f for f in pack.facts if "style" in f.key]
        assert len(style_facts) > 0

    def test_import_claude_french_detection(self, tmp_path):
        """Test French language detection."""
        from aura.importers.claude import import_claude_export

        data = [
            {
                "chat_messages": [
                    {"sender": "human", "text": "Je veux créer une application web avec React"},
                    {"sender": "human", "text": "Est-ce que tu peux m'aider avec le backend?"},
                    {"sender": "human", "text": "Je travaille dans une entreprise de technologie"},
                    {"sender": "human", "text": "Comment est-ce que je peux déployer sur Vercel?"},
                    {"sender": "human", "text": "Merci pour ton aide, c'est exactement ce que je cherchais"},
                ],
            },
        ]

        json_path = tmp_path / "french.json"
        json_path.write_text(json.dumps(data))

        pack = import_claude_export(json_path)
        style_facts = [f for f in pack.facts if "language" in f.key and f.key.startswith("style")]
        assert len(style_facts) > 0
        assert "French" in style_facts[0].value


# ---------------------------------------------------------------------------
# Compact Profile (Token Efficiency)
# ---------------------------------------------------------------------------
class TestCompactProfile:
    def test_compact_profile_basic(self, dev_pack):
        from aura.mcp_server import _compact_profile
        result = _compact_profile([dev_pack])
        assert isinstance(result, str)
        assert len(result) > 0
        # Should be much shorter than full export
        full = export_system_prompt([dev_pack], include_header=False)
        assert len(result) < len(full)

    def test_compact_profile_with_max_facts(self, dev_pack):
        from aura.mcp_server import _compact_profile
        result_all = _compact_profile([dev_pack])
        result_limited = _compact_profile([dev_pack], max_facts=1)
        assert len(result_limited) <= len(result_all)

    def test_compact_profile_empty(self):
        from aura.mcp_server import _compact_profile
        result = _compact_profile([])
        assert "No user context" in result


# ---------------------------------------------------------------------------
# Memory Decay Tests
# ---------------------------------------------------------------------------
class TestMemoryDecay:
    def test_identity_never_expires(self):
        from aura.consolidate import check_decay
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta
        from datetime import datetime, timedelta

        old_date = datetime.now() - timedelta(days=365)
        pack = ContextPack(
            name="test", scope="general",
            facts=[
                Fact(key="identity.name", value="Nok", type=FactType.IDENTITY,
                     confidence=Confidence.HIGH, updated_at=old_date),
            ],
            meta=PackMeta(description="Test"),
        )
        result = check_decay([pack])
        assert len(result.expired) == 0
        assert result.preserved == 1

    def test_context_fact_expires(self):
        from aura.consolidate import check_decay
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta
        from datetime import datetime, timedelta

        old_date = datetime.now() - timedelta(days=120)
        pack = ContextPack(
            name="test", scope="general",
            facts=[
                Fact(key="project.current", value="old-project", type=FactType.CONTEXT,
                     confidence=Confidence.HIGH, updated_at=old_date),
            ],
            meta=PackMeta(description="Test"),
        )
        result = check_decay([pack])
        assert len(result.expired) == 1

    def test_low_confidence_decays_faster(self):
        from aura.consolidate import compute_ttl
        from aura.schema import Fact, FactType, Confidence

        high = Fact(key="x", value="y", type=FactType.CONTEXT, confidence=Confidence.HIGH)
        low = Fact(key="x", value="y", type=FactType.CONTEXT, confidence=Confidence.LOW)
        assert compute_ttl(low) < compute_ttl(high)

    def test_import_decays_faster(self):
        from aura.consolidate import compute_ttl
        from aura.schema import Fact, FactType, Confidence

        manual = Fact(key="x", value="y", type=FactType.CONTEXT, confidence=Confidence.HIGH, source="manual")
        imported = Fact(key="x", value="y", type=FactType.CONTEXT, confidence=Confidence.HIGH, source="chatgpt-import")
        assert compute_ttl(imported) < compute_ttl(manual)

    def test_decay_apply_removes_facts(self):
        from aura.consolidate import check_decay
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta
        from datetime import datetime, timedelta

        old_date = datetime.now() - timedelta(days=200)
        pack = ContextPack(
            name="test", scope="general",
            facts=[
                Fact(key="old.fact", value="stale", type=FactType.CONTEXT,
                     confidence=Confidence.HIGH, updated_at=old_date),
                Fact(key="identity.name", value="Nok", type=FactType.IDENTITY,
                     confidence=Confidence.HIGH, updated_at=old_date),
            ],
            meta=PackMeta(description="Test"),
        )
        result = check_decay([pack], dry_run=False)
        assert len(result.expired) == 1
        assert len(pack.facts) == 1
        assert pack.facts[0].key == "identity.name"

    def test_warning_expiring_soon(self):
        from aura.consolidate import check_decay
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta
        from datetime import datetime, timedelta

        almost_old = datetime.now() - timedelta(days=80)
        pack = ContextPack(
            name="test", scope="general",
            facts=[
                Fact(key="project.soon", value="expiring", type=FactType.CONTEXT,
                     confidence=Confidence.HIGH, updated_at=almost_old),
            ],
            meta=PackMeta(description="Test"),
        )
        result = check_decay([pack])
        assert len(result.warning) == 1


# ---------------------------------------------------------------------------
# Consolidation Tests
# ---------------------------------------------------------------------------
class TestConsolidation:
    def test_find_cross_pack_duplicates(self):
        from aura.consolidate import consolidate
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta

        pack_a = ContextPack(
            name="dev", scope="development",
            facts=[Fact(key="editor", value="Cursor", type=FactType.PREFERENCE, confidence=Confidence.HIGH)],
            meta=PackMeta(description="Dev"),
        )
        pack_b = ContextPack(
            name="work", scope="work",
            facts=[Fact(key="editor", value="Cursor", type=FactType.PREFERENCE, confidence=Confidence.MEDIUM)],
            meta=PackMeta(description="Work"),
        )
        result = consolidate([pack_a, pack_b])
        assert len(result.merged) == 1

    def test_find_contradictions(self):
        from aura.consolidate import consolidate
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta

        pack_a = ContextPack(
            name="dev", scope="development",
            facts=[Fact(key="editor", value="Cursor", type=FactType.PREFERENCE, confidence=Confidence.HIGH)],
            meta=PackMeta(description="Dev"),
        )
        pack_b = ContextPack(
            name="work", scope="work",
            facts=[Fact(key="editor", value="VS Code", type=FactType.PREFERENCE, confidence=Confidence.HIGH)],
            meta=PackMeta(description="Work"),
        )
        result = consolidate([pack_a, pack_b])
        assert len(result.contradictions) == 1

    def test_consolidate_apply_removes(self):
        from aura.consolidate import consolidate
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta

        pack_a = ContextPack(
            name="dev", scope="development",
            facts=[Fact(key="lang", value="Python", type=FactType.SKILL, confidence=Confidence.HIGH)],
            meta=PackMeta(description="Dev"),
        )
        pack_b = ContextPack(
            name="work", scope="work",
            facts=[Fact(key="lang", value="Python", type=FactType.SKILL, confidence=Confidence.LOW)],
            meta=PackMeta(description="Work"),
        )
        result = consolidate([pack_a, pack_b], dry_run=False)
        assert result.removed == 1
        assert len(pack_b.facts) == 0

    def test_no_duplicates_clean(self):
        from aura.consolidate import consolidate
        from aura.schema import ContextPack, Fact, FactType, Confidence, PackMeta

        pack = ContextPack(
            name="dev", scope="development",
            facts=[
                Fact(key="lang", value="Python", type=FactType.SKILL, confidence=Confidence.HIGH),
                Fact(key="editor", value="Cursor", type=FactType.PREFERENCE, confidence=Confidence.HIGH),
            ],
            meta=PackMeta(description="Dev"),
        )
        result = consolidate([pack])
        assert len(result.merged) == 0
        assert len(result.contradictions) == 0
