import json
import os
import shutil
import subprocess
import time
import uuid
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parent.parent
UNIQUE_TERM = "zzuniqtestterm42"
SYNONYM = "unique test concept"


def _rg_bin() -> str:
    rg = shutil.which("rg") or "/root/.cache/opencode/bin/rg"
    if not Path(rg).exists():
        pytest.skip("ripgrep not available")
    return rg


def _rg_search(pattern: str, memory_dir: Path) -> list[str]:
    rg = _rg_bin()
    result = subprocess.run(
        [
            rg,
            "-il",
            "--glob",
            "*.md",
            "--glob",
            "!.git",
            "--glob",
            "!.rag",
            "-e",
            pattern,
            str(memory_dir),
        ],
        capture_output=True,
        text=True,
        timeout=30,
        check=False,
    )
    if result.returncode != 0:
        return []
    return [line.strip() for line in result.stdout.strip().splitlines() if line.strip()]


def _semantic_search(
    query: str, index_dir: Path, env: dict[str, str] | None = None, timeout: int = 120
) -> list[dict]:
    run_env = env if env is not None else dict(os.environ)
    result = subprocess.run(
        [
            "python3",
            "-m",
            "src.memory",
            "search",
            query,
            "-i",
            str(index_dir),
            "-k",
            "15",
            "--json",
        ],
        capture_output=True,
        text=True,
        timeout=timeout,
        cwd=str(REPO_ROOT),
        env=run_env,
        check=False,
    )
    if result.returncode != 0:
        return []
    try:
        parsed = json.loads(result.stdout)
    except json.JSONDecodeError:
        return []
    return parsed if isinstance(parsed, list) else []


def _reindex(
    memory_dir: Path, index_dir: Path, env: dict[str, str] | None = None, timeout: int = 180
) -> None:
    index_dir.mkdir(parents=True, exist_ok=True)
    run_env = env if env is not None else dict(os.environ)
    result = subprocess.run(
        [
            "python3",
            "-m",
            "src.memory",
            "index",
            str(memory_dir),
            "-o",
            str(index_dir),
        ],
        capture_output=True,
        text=True,
        timeout=timeout,
        cwd=str(REPO_ROOT),
        env=run_env,
        check=False,
    )
    assert result.returncode == 0, (
        f"reindex failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
    )
    log_path = index_dir / "reindex.log"
    deadline = time.monotonic() + 30
    while time.monotonic() < deadline:
        if log_path.exists():
            content = log_path.read_text(encoding="utf-8")
            if "done:" in content or "failed:" in content:
                return
        time.sleep(1)
    pytest.fail("reindex did not write done/failed to log in time")


def _make_memory_file(memory_dir: Path, term: str, synonym: str = "") -> Path:
    uid = uuid.uuid4().hex[:8]
    tech_dir = memory_dir / "technical"
    tech_dir.mkdir(parents=True, exist_ok=True)
    fname = tech_dir / f"e2e-{uid}.md"
    body = (
        f"---\ntitle: E2E test {uid}\ntags: [e2e, test]\n"
        f"summary: E2E coverage probe\n---\n\n# E2E test\n\n"
        f"Contains {term} as exact marker. {synonym}\n"
    )
    fname.write_text(body, encoding="utf-8")
    return fname


def _make_dummy_file(memory_dir: Path) -> Path:
    tech_dir = memory_dir / "technical"
    tech_dir.mkdir(parents=True, exist_ok=True)
    fname = tech_dir / "dummy.md"
    fname.write_text(
        "---\ntitle: dummy\n---\n\n# Dummy\n\nfiller content for reindex stability\n",
        encoding="utf-8",
    )
    return fname


def test_zero_config_keyword_only(tmp_path: Path) -> None:
    """Scenario 1: keyword works without OPENAI_BASE_URL, semantic returns []."""
    memory_dir = tmp_path / "memory"
    index_dir = memory_dir / ".rag"
    fname = _make_memory_file(memory_dir, UNIQUE_TERM)
    rel_path = fname.relative_to(memory_dir).as_posix()

    env_no_base = {k: v for k, v in os.environ.items() if k != "OPENAI_BASE_URL"}

    semantic = _semantic_search(UNIQUE_TERM, index_dir, env=env_no_base)
    keyword = _rg_search(UNIQUE_TERM, memory_dir)

    assert semantic == [], f"semantic should return [] without OPENAI_BASE_URL, got {semantic}"
    assert any(rel_path in p for p in keyword), f"keyword should find {rel_path}, got {keyword}"


@pytest.mark.timeout(180)
@pytest.mark.skipif(
    not os.environ.get("RUN_LIVE"),
    reason="needs OPENAI_BASE_URL for semantic search",
)
def test_full_hybrid_semantic_and_keyword(tmp_path: Path) -> None:
    """Scenario 2: semantic finds by synonym, keyword finds by exact term."""
    memory_dir = tmp_path / "memory"
    index_dir = memory_dir / ".rag"
    fname = _make_memory_file(memory_dir, UNIQUE_TERM, SYNONYM)
    _make_dummy_file(memory_dir)
    rel_path = fname.relative_to(memory_dir).as_posix()

    _reindex(memory_dir, index_dir)

    semantic_term = _semantic_search(UNIQUE_TERM, index_dir)
    semantic_synonym = _semantic_search(SYNONYM, index_dir)
    keyword = _rg_search(UNIQUE_TERM, memory_dir)

    sem_sources_term = {r["source"] for r in semantic_term}
    sem_sources_syn = {r["source"] for r in semantic_synonym}
    assert rel_path in sem_sources_term, (
        f"semantic should find by exact term, got {sem_sources_term}"
    )
    assert rel_path in sem_sources_syn, f"semantic should find by synonym, got {sem_sources_syn}"
    assert any(rel_path in p for p in keyword), f"keyword should find {rel_path}, got {keyword}"


@pytest.mark.timeout(300)
@pytest.mark.skipif(
    not os.environ.get("RUN_LIVE"),
    reason="needs OPENAI_BASE_URL for semantic search",
)
def test_fallback_openrouter_down(tmp_path: Path) -> None:
    """Scenario 3: keyword works when OpenRouter returns 401."""
    memory_dir = tmp_path / "memory"
    index_dir = memory_dir / ".rag"
    fname = _make_memory_file(memory_dir, UNIQUE_TERM, SYNONYM)
    _make_dummy_file(memory_dir)
    rel_path = fname.relative_to(memory_dir).as_posix()

    _reindex(memory_dir, index_dir)

    env_invalid_key = dict(os.environ)
    env_invalid_key["OPENAI_API_KEY"] = "invalid_key"

    semantic = _semantic_search(UNIQUE_TERM, index_dir, env=env_invalid_key)
    keyword = _rg_search(UNIQUE_TERM, memory_dir)

    assert semantic == [], f"semantic should return [] with invalid key, got {semantic}"
    assert any(rel_path in p for p in keyword), (
        f"keyword fallback should find {rel_path}, got {keyword}"
    )


@pytest.mark.timeout(180)
@pytest.mark.skipif(
    not os.environ.get("RUN_LIVE"),
    reason="needs OPENAI_BASE_URL for semantic search",
)
def test_file_deletion(tmp_path: Path) -> None:
    """Scenario 4: after deletion + reindex, search returns []."""
    memory_dir = tmp_path / "memory"
    index_dir = memory_dir / ".rag"
    fname = _make_memory_file(memory_dir, UNIQUE_TERM, SYNONYM)
    _make_dummy_file(memory_dir)
    rel_path = fname.relative_to(memory_dir).as_posix()

    _reindex(memory_dir, index_dir)

    fname.unlink()
    _reindex(memory_dir, index_dir)

    semantic = _semantic_search(UNIQUE_TERM, index_dir)
    keyword = _rg_search(UNIQUE_TERM, memory_dir)

    sem_sources = {r["source"] for r in semantic}
    assert rel_path not in sem_sources, (
        f"deleted file should not be in semantic results, got {sem_sources}"
    )
    assert not any(rel_path in p for p in keyword), (
        f"deleted file should not be in keyword results, got {keyword}"
    )
