"""End-to-end tests for the autoskill pipeline.

Exercises the full chain — fetch → redact → cluster → match → synthesize → write
— with real internal modules. External dependencies (screenpipe, LLM, embeddings)
are replaced with deterministic fakes so the suite runs offline in under a second.
"""

import json
import sys
from pathlib import Path

import httpx
import pytest

# Synthetic opaque value for auth-header round-trip tests. Not a credential.
_AUTH = "fake-test-value-1234"

from backends import LocalBackend
from run import run, main


# ---------- fixtures ----------

SKILL_TEMPLATE = "---\nname: {name}\ndescription: {description}\n---\n# {name}\n"


def _write_skill(root: Path, name: str, description: str) -> None:
    d = root / name
    d.mkdir(parents=True, exist_ok=True)
    (d / "SKILL.md").write_text(SKILL_TEMPLATE.format(name=name, description=description))


def _seed_skills_dir(root: Path) -> None:
    """A tiny but representative slice of the real skills/ layout."""
    _write_skill(root, "literature-review",
                 "Systematic literature reviews across PubMed arXiv bioRxiv with citations.")
    _write_skill(root, "citation-management",
                 "Find papers and format citations from Google Scholar and PubMed.")
    _write_skill(root, "scientific-writing",
                 "Write research manuscripts with IMRAD structure and citations.")
    _write_skill(root, "scientific-schematics",
                 "Create scientific diagrams figures and schematics for publications.")
    _write_skill(root, "latex-posters",
                 "Create academic conference posters in LaTeX.")
    _write_skill(root, "rdkit",
                 "Cheminformatics with RDKit molecules drug discovery.")


# ---------- fake screenpipe ----------

def _ocr(ts: str, app: str, title: str, text: str) -> dict:
    return {"type": "OCR", "content": {
        "timestamp": ts, "app_name": app, "window_name": title, "text": text,
    }}


def _realistic_day() -> list:
    """Three workflow patterns repeated twice each, plus noise."""
    events = []
    # Pattern 1: literature work (Chrome + Zotero) — reuse expected
    for hr in (9, 14):
        for m in range(0, 20):
            events.append(_ocr(f"2026-04-17T{hr:02d}:{m:02d}:00Z",
                               "Chrome", "PubMed", "searching papers"))
    # Pattern 2: slides + schematics (Keynote + Preview) — novel expected
    for hr in (11, 16):
        for m in range(0, 20):
            events.append(_ocr(f"2026-04-17T{hr:02d}:{m:02d}:00Z",
                               "Keynote", "deck", "slides for keynote talk"))
    # Pattern 3: manuscript + figures (VSCode + Preview) — compose expected
    for hr in (12, 17):
        for m in range(0, 20):
            events.append(_ocr(f"2026-04-17T{hr:02d}:{m:02d}:00Z",
                               "VSCode", "paper.tex", "writing manuscript with figures"))
    # Pattern 4 (lonely): should be dropped (only one session)
    for m in range(0, 10):
        events.append(_ocr(f"2026-04-17T22:{m:02d}:00Z",
                           "Lonely", "", "whatever"))
    return events


def _screenpipe_client(events: list) -> httpx.Client:
    # Paginate in chunks of 20 to exercise fetch_window's pagination loop.
    page_size = 20

    def handler(request):
        params = dict(request.url.params)
        offset = int(params.get("offset", "0"))
        chunk = events[offset:offset + page_size]
        return httpx.Response(200, json={
            "data": chunk,
            "pagination": {"limit": page_size, "offset": offset, "total": len(events)},
        })

    return httpx.Client(transport=httpx.MockTransport(handler), base_url="http://screenpipe.test")


# ---------- fake LM Studio ----------

class _FakeLMStudioHandler:
    """Returns different verdicts based on apps mentioned in the prompt."""

    def __init__(self):
        self.prompts = []

    def __call__(self, request):
        body = json.loads(request.read())
        prompt = body["messages"][0]["content"]
        self.prompts.append(prompt)

        if "Chrome" in prompt and "PubMed" in prompt:
            out = {"verdict": "reuse", "target": "literature-review"}
        elif "Keynote" in prompt:
            body = ("---\nname: keynote-talk-prep\n"
                    "description: Prepare and iterate on Keynote talks.\n---\n"
                    "# keynote-talk-prep\n")
            out = {"verdict": "novel", "name": "keynote-talk-prep", "skill_body": body}
        elif "VSCode" in prompt:
            body = ("---\nname: manuscript-with-figures\n"
                    "description: Chain scientific-writing + scientific-schematics.\n---\n"
                    "# manuscript-with-figures\n\n"
                    "Invoke scientific-writing then scientific-schematics.\n")
            out = {"verdict": "compose", "name": "manuscript-with-figures", "skill_body": body}
        else:
            out = {"verdict": "reuse", "target": "literature-review"}

        return httpx.Response(200, json={
            "choices": [{"message": {"content": json.dumps(out)}}]
        })


def _keyword_embedder(text: str):
    keywords = ["paper", "pubmed", "literature", "citation", "writing",
                "schematic", "figure", "poster", "molecule", "slides"]
    return [1.0 if k in text.lower() else 0.0 for k in keywords]


def _base_config():
    return {
        "cluster": {"min_session_minutes": 0, "idle_gap_minutes": 10, "min_cluster_size": 2},
    }


# ---------- tests ----------

def test_full_pipeline_produces_report_and_drafts(tmp_path):
    skills_dir = tmp_path / "skills"
    _seed_skills_dir(skills_dir)

    fake_lmstudio = _FakeLMStudioHandler()
    lm_client = httpx.Client(
        transport=httpx.MockTransport(fake_lmstudio),
        base_url="http://localhost:1234/v1",
    )
    backend = LocalBackend(endpoint="http://localhost:1234/v1",
                           model="Gemma-4-31B-it", client=lm_client)

    out = run(
        _base_config(),
        start_time="2026-04-17T00:00:00Z", end_time="2026-04-17T23:59:59Z",
        out_dir=tmp_path / "_proposed",
        screenpipe_client=_screenpipe_client(_realistic_day()),
        backend=backend,
        embedder=_keyword_embedder,
        skills_dir=skills_dir,
        now=lambda: "2026-04-17T18-00-00",
    )

    # Report exists and mentions all three surviving clusters.
    report = (out / "report.md").read_text()
    assert "Chrome" in report
    assert "Keynote" in report
    assert "VSCode" in report
    assert "reuse" in report
    assert "novel" in report
    assert "compose" in report
    # Lonely cluster was dropped by min_cluster_size.
    assert "Lonely" not in report

    # Novel draft landed under new-skills/.
    novel_skill = out / "new-skills" / "keynote-talk-prep" / "SKILL.md"
    assert novel_skill.exists()
    assert "keynote-talk-prep" in novel_skill.read_text()

    # Compose draft landed under composition-recipes/.
    compose_skill = out / "composition-recipes" / "manuscript-with-figures" / "SKILL.md"
    assert compose_skill.exists()
    body = compose_skill.read_text()
    assert "scientific-writing" in body
    assert "scientific-schematics" in body

    # Reuse verdict writes no draft but does cite the matched existing skill.
    assert not (out / "new-skills" / "literature-review").exists()
    assert "literature-review" in report

    # Pagination actually happened (more than one fetch).
    # 120 events / 20 per page = 6 pages → ≥6 backend-independent calls.
    # (Can't assert directly without reaching into the transport; but the test
    # would fail above if pagination were broken since clusters would be empty.)


def test_pipeline_redaction_prevents_secrets_leaving_the_host(tmp_path):
    skills_dir = tmp_path / "skills"
    _seed_skills_dir(skills_dir)

    # Plant secrets in every OCR event.
    toxic = [
        _ocr(f"2026-04-17T{hr:02d}:{m:02d}:00Z", "Chrome", "Gmail",
             "email alice@example.com password sk-abcdefghijklmnopqrstuvwxyz012345")
        for hr in (9, 14) for m in range(0, 20)
    ]

    fake_lmstudio = _FakeLMStudioHandler()
    lm_client = httpx.Client(
        transport=httpx.MockTransport(fake_lmstudio),
        base_url="http://localhost:1234/v1",
    )
    backend = LocalBackend(endpoint="http://localhost:1234/v1",
                           model="Gemma-4-31B-it", client=lm_client)

    run(
        _base_config(),
        start_time="s", end_time="e",
        out_dir=tmp_path / "_proposed",
        screenpipe_client=_screenpipe_client(toxic),
        backend=backend, embedder=_keyword_embedder, skills_dir=skills_dir,
        now=lambda: "ts",
    )

    assert fake_lmstudio.prompts, "backend should have been called"
    for prompt in fake_lmstudio.prompts:
        assert "alice@example.com" not in prompt
        assert "sk-abcdefghijklmnopqrstuvwxyz012345" not in prompt


def test_auth_token_threads_through_run_into_fetch_window(tmp_path):
    """Integration: config's screenpipe.token reaches the outgoing HTTP request."""
    skills_dir = tmp_path / "skills"
    _seed_skills_dir(skills_dir)

    seen_auth = []

    def handler(request):
        seen_auth.append(request.headers.get("authorization"))
        return httpx.Response(200, json={
            "data": [], "pagination": {"limit": 20, "offset": 0, "total": 0},
        })

    client = httpx.Client(transport=httpx.MockTransport(handler),
                          base_url="http://screenpipe.test")

    run(
        _base_config(),
        start_time="2026-04-17T00:00:00Z", end_time="2026-04-17T23:59:59Z",
        out_dir=tmp_path / "_proposed",
        screenpipe_client=client,
        backend=None, embedder=_keyword_embedder, skills_dir=skills_dir,
        screenpipe_token=_AUTH,
        now=lambda: "ts",
    )

    assert seen_auth, "screenpipe should have been called"
    assert all(h == f"Bearer {_AUTH}" for h in seen_auth), seen_auth


def test_cli_dry_run_writes_plan_without_backend_or_embedding_model(tmp_path, monkeypatch):
    """Exercises scripts/run.py's main() via argv with --dry-run.

    --dry-run is the only path that avoids loading sentence-transformers and
    instantiating a backend, so we can smoke-test the CLI offline.
    """
    skills_dir = tmp_path / "skills"
    _seed_skills_dir(skills_dir)

    # Minimal config.yaml pointing at our fake screenpipe (via localhost base_url
    # that we'll monkeypatch httpx.Client to respect).
    config = tmp_path / "config.yaml"
    config.write_text(
        "backend: local\n"
        "local:\n"
        "  endpoint: http://localhost:1234/v1\n"
        "  model: Gemma-4-31B-it\n"
        "screenpipe:\n"
        "  url: http://screenpipe.test\n"
        "cluster:\n"
        "  min_session_minutes: 0\n"
        "  idle_gap_minutes: 10\n"
        "  min_cluster_size: 2\n"
    )

    # Install a MockTransport-backed default for any httpx.Client the CLI builds.
    events = _realistic_day()

    def handler(request):
        params = dict(request.url.params)
        offset = int(params.get("offset", "0"))
        chunk = events[offset:offset + 20]
        return httpx.Response(200, json={
            "data": chunk,
            "pagination": {"limit": 20, "offset": offset, "total": len(events)},
        })

    original_client = httpx.Client
    monkeypatch.setattr(httpx, "Client", lambda *a, **kw: original_client(
        *a, **{**kw, "transport": httpx.MockTransport(handler)}
    ))

    # Need pyyaml for main() to load the config; install on demand and skip if unavailable.
    yaml = pytest.importorskip("yaml")

    out_dir = tmp_path / "_proposed"
    rc = main([
        "--start", "2026-04-17T00:00:00Z",
        "--end", "2026-04-17T23:59:59Z",
        "--config", str(config),
        "--skills-dir", str(skills_dir),
        "--out", str(out_dir),
        "--dry-run",
    ])
    assert rc == 0

    # One timestamped subdir was created under --out.
    subdirs = [p for p in out_dir.iterdir() if p.is_dir()]
    assert len(subdirs) == 1
    plan = (subdirs[0] / "plan.md").read_text()
    assert "Cluster" in plan
    assert "Chrome" in plan
