"""Imported source (MCP attachments) is preserved as a checksummed snapshot in the project's
private meta directory, committed with the dispatch, and never leaves platform-owned paths.
See import_snapshot.py."""

from __future__ import annotations

import hashlib
import json
import os
import tempfile
from datetime import timedelta
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from src.domain.agent_access.operations import StartBuild
from src.domain.agent_access.service import utcnow
from src.domain.codegen.agent_facade import AgentCodegenFacade
from src.domain.codegen.import_snapshot import (
    IMPORT_DIR,
    ImportSnapshotError,
    build_import_appendix,
    load_for_dispatch,
    normalize_import_path,
    read_snapshot_files,
    repo_files,
    snapshots_dir,
    summarize,
    write_snapshot,
)
from src.domain.codegen.repo_export import is_platform_owned_path

SOURCE = {"kind": "mcp", "prompt_source": "mcp_agent", "grant_id": "g"}
HTML = "<!doctype html>\n<h1>Exact design</h1>\n"
KEY = "sk_live_" + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4"  # shaped like a hardcoded secret


def _sha(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()


@pytest.fixture
def base():
    with tempfile.TemporaryDirectory() as directory:
        yield Path(directory)


# ---- paths ------------------------------------------------------------------


@pytest.mark.parametrize(
    "raw,reason",
    [
        ("../etc/passwd", "non_normalized_path"),
        ("a/../../b.js", "non_normalized_path"),
        ("/abs/index.html", "absolute_path"),
        ("C:/x/index.html", "absolute_path"),
        ("a\\b.js", "backslash_in_path"),
        ("a//b.js", "non_normalized_path"),
        (".git/config", "git_internals_path"),
        ("src/.GIT/hooks/x", "git_internals_path"),
        ("bad\x00name.js", "control_character_in_path"),
        ("", "empty_or_padded_path"),
        (" index.html", "empty_or_padded_path"),
        ("x" * 201, "path_too_long"),
    ],
)
def test_unusable_paths_are_rejected_with_a_reason(raw, reason):
    with pytest.raises(ValueError, match=reason):
        normalize_import_path(raw)


def test_ordinary_paths_pass_and_a_leading_dot_slash_is_stripped():
    assert normalize_import_path("css/site.scss") == "css/site.scss"
    assert normalize_import_path("./index.html") == "index.html"
    # Inert inside the snapshot; the prompt forbids copying these into the app root.
    assert normalize_import_path(".github/workflows/x.yml") == ".github/workflows/x.yml"


# ---- writing / reading ------------------------------------------------------


def test_snapshot_records_size_hash_and_lines_and_is_byte_exact(base):
    manifest = write_snapshot(base, [("index.html", HTML), ("css/a.css", "a{}")], source=SOURCE)
    by_path = {f["path"]: f for f in manifest["files"]}
    assert by_path["index.html"]["sha256"] == _sha(HTML)
    assert by_path["index.html"]["received_sha256"] == _sha(HTML)
    assert by_path["index.html"]["size"] == len(HTML.encode())
    assert by_path["index.html"]["lines"] == 2
    assert by_path["index.html"]["redacted"] is False
    on_disk = base / manifest["import_id"] / "files" / "index.html"
    assert on_disk.read_text(encoding="utf-8") == HTML
    saved = json.loads((base / manifest["import_id"] / "manifest.json").read_text("utf-8"))
    assert saved == manifest and manifest["source"] == SOURCE


def test_unusable_and_duplicate_paths_are_reported_not_silently_dropped(base):
    manifest = write_snapshot(
        base,
        [("ok.js", "1"), ("../evil.js", "2"), ("OK.js", "3"), (".git/config", "4")],
        source=SOURCE,
    )
    assert [f["path"] for f in manifest["files"]] == ["ok.js"]
    assert {(r["path"], r["reason"]) for r in manifest["rejected"]} == {
        ("../evil.js", "non_normalized_path"),
        ("OK.js", "duplicate_path"),
        (".git/config", "git_internals_path"),
    }
    assert not (base / "evil.js").exists()  # nothing escaped the snapshot directory


def test_secrets_are_redacted_before_storing_and_flagged(base):
    text = f"const stripe = '{KEY}';\n"
    manifest = write_snapshot(base, [("app.js", text)], source=SOURCE)
    entry = manifest["files"][0]
    stored = (base / manifest["import_id"] / "files" / "app.js").read_text("utf-8")
    assert KEY not in stored
    assert entry["redacted"] is True
    assert entry["received_sha256"] == _sha(text) and entry["sha256"] == _sha(stored)
    assert entry["sha256"] != entry["received_sha256"]


def test_round_trip_and_repo_paths_sit_under_a_platform_owned_directory(base):
    manifest = write_snapshot(base, [("index.html", HTML), ("js/app.js", "x()")], source=SOURCE)
    contents = read_snapshot_files(base, manifest)
    files = repo_files(manifest, contents)
    prefix = f"{IMPORT_DIR}/{manifest['import_id']}"
    assert files[f"{prefix}/files/index.html"] == HTML.encode()
    assert json.loads(files[f"{prefix}/manifest.json"])["import_id"] == manifest["import_id"]
    for repo_path in files:
        assert is_platform_owned_path(repo_path.split("/")), repo_path


def test_a_tampered_or_missing_file_is_detected_not_committed_as_the_original(base):
    manifest = write_snapshot(base, [("index.html", HTML)], source=SOURCE)
    target = base / manifest["import_id"] / "files" / "index.html"
    target.write_text("<h1>changed</h1>", encoding="utf-8")
    with pytest.raises(ImportSnapshotError, match="changed on disk"):
        read_snapshot_files(base, manifest)
    target.unlink()
    with pytest.raises(ImportSnapshotError, match="missing"):
        read_snapshot_files(base, manifest)


def test_a_manifest_cannot_point_outside_the_snapshot_directory(base):
    with pytest.raises(ImportSnapshotError):
        read_snapshot_files(base, {"import_id": "../../etc", "files": []})
    manifest = write_snapshot(base, [("index.html", HTML)], source=SOURCE)
    manifest["files"][0]["path"] = "../../secret"
    with pytest.raises(ValueError):
        read_snapshot_files(base, manifest)


def test_the_summary_carries_hashes_and_sizes_never_contents(base):
    manifest = write_snapshot(base, [("index.html", HTML)], source=SOURCE)
    summary = summarize(manifest)
    assert summary["files"][0]["sha256"] == _sha(HTML)
    assert "Exact design" not in json.dumps(summary)


def test_the_appendix_tells_the_agent_to_copy_and_where_the_originals_are(base):
    text = f"const k = '{KEY}';"
    manifest = write_snapshot(base, [("index.html", HTML), ("a.js", text)], source=SOURCE)
    appendix = build_import_appendix(manifest)
    prefix = f"{IMPORT_DIR}/{manifest['import_id']}"
    assert f"{prefix}/files/" in appendix
    assert "Do NOT re-type" in appendix
    assert "`a.js`" in appendix and "(secrets redacted)" in appendix
    assert KEY not in appendix and "Exact design" not in appendix


# ---- where snapshots live ----------------------------------------------------


def test_snapshots_live_in_the_private_project_meta_dir_not_the_public_media_root():
    settings = SimpleNamespace(
        repo_workspace="/var/p2hs/repos", prompt_media_upload_root="/srv/media/public"
    )
    workspace_id, project_id = uuid4(), uuid4()
    path = snapshots_dir(settings, workspace_id, project_id).as_posix()
    assert f"/{workspace_id}/project_meta/{project_id}/imports" in path
    assert "/srv/media/public" not in path
    assert path.startswith(Path("/var/p2hs/repos").as_posix()) or "var/p2hs/repos" in path


# ---- dispatch -----------------------------------------------------------------


@pytest.mark.asyncio
async def test_dispatch_loads_verified_files_and_an_appendix():
    with tempfile.TemporaryDirectory() as root:
        settings = SimpleNamespace(repo_workspace=root)
        workspace_id, project_id = uuid4(), uuid4()
        manifest = write_snapshot(
            snapshots_dir(settings, workspace_id, project_id),
            [("index.html", HTML)],
            source=SOURCE,
        )
        files, appendix = await load_for_dispatch(
            settings, workspace_id, project_id, {"import_snapshot": manifest}
        )
    assert f"{IMPORT_DIR}/{manifest['import_id']}/files/index.html" in files
    assert "Imported source, preserved unchanged" in appendix


@pytest.mark.asyncio
async def test_dispatch_degrades_to_the_inline_prompt_instead_of_failing_the_run():
    """The prompt still carries the source, so a missing/corrupt snapshot must never block."""
    with tempfile.TemporaryDirectory() as root:
        settings = SimpleNamespace(repo_workspace=root)
        workspace_id, project_id = uuid4(), uuid4()
        manifest = write_snapshot(
            snapshots_dir(settings, workspace_id, project_id), [("a.js", "1")], source=SOURCE
        )
        (
            snapshots_dir(settings, workspace_id, project_id)
            / manifest["import_id"]
            / "files"
            / "a.js"
        ).write_text("tampered", encoding="utf-8")
        assert await load_for_dispatch(
            settings, workspace_id, project_id, {"import_snapshot": manifest}
        ) == ({}, "")
    assert await load_for_dispatch(settings, uuid4(), uuid4(), {}) == ({}, "")
    assert await load_for_dispatch(settings, uuid4(), uuid4(), None) == ({}, "")


# ---- start_build wiring ---------------------------------------------------------


def _facade(workspace_id, project_id):
    facade = AgentCodegenFacade(MagicMock())
    facade.projects.get_by_id = AsyncMock(
        return_value=SimpleNamespace(id=project_id, workspace_id=workspace_id, discovery_json=None)
    )
    facade.service = SimpleNamespace(create_run=AsyncMock(return_value=SimpleNamespace(id=uuid4())))
    return facade


def _grant():
    return SimpleNamespace(id=uuid4(), created_by=uuid4(), expires_at=utcnow() + timedelta(days=1))


@pytest.mark.asyncio
async def test_start_build_snapshots_attachments_and_carries_only_the_manifest():
    workspace_id, project_id = uuid4(), uuid4()
    with tempfile.TemporaryDirectory() as root:
        settings = SimpleNamespace(repo_workspace=root, prompt_media_upload_root=root)
        facade = _facade(workspace_id, project_id)
        body = StartBuild(
            request_key="build-snapshot-1",
            project_id=project_id,
            prompt="Make it functional",
            attachments=[{"path": "index.html", "content": HTML}],
        )
        with patch("src.domain.codegen.agent_facade.get_settings", return_value=settings):
            await facade.start(_grant(), body, prompt_source="mcp_agent")
        request = facade.service.create_run.await_args.kwargs["generation_request"]
        manifest = request["import_snapshot"]
        stored = snapshots_dir(settings, workspace_id, project_id) / manifest["import_id"]
        assert (stored / "files" / "index.html").read_text("utf-8") == HTML
    assert manifest["files"][0]["sha256"] == _sha(HTML)
    assert manifest["source"]["prompt_source"] == "mcp_agent"
    assert HTML not in json.dumps(manifest)  # contents never ride in the plan context
    # The inline prompt copy is untouched, so every executor path keeps working.
    assert "Exact design" in facade.service.create_run.await_args.kwargs["user_prompt"]


@pytest.mark.asyncio
async def test_a_disk_problem_never_fails_the_build():
    workspace_id, project_id = uuid4(), uuid4()
    facade = _facade(workspace_id, project_id)
    body = StartBuild(
        request_key="build-snapshot-2",
        project_id=project_id,
        prompt="Make it functional",
        attachments=[{"path": "index.html", "content": HTML}],
    )
    with patch("src.domain.codegen.agent_facade.write_snapshot", side_effect=OSError("disk full")):
        await facade.start(_grant(), body)
    request = facade.service.create_run.await_args.kwargs["generation_request"]
    assert request["import_snapshot"] is None
    assert "Exact design" in facade.service.create_run.await_args.kwargs["user_prompt"]


@pytest.mark.asyncio
async def test_no_attachments_means_no_snapshot():
    facade = _facade(uuid4(), uuid4())
    body = StartBuild(request_key="build-snapshot-3", project_id=uuid4(), prompt="Just a prompt")
    await facade.start(_grant(), body)
    assert facade.service.create_run.await_args.kwargs["generation_request"] is None
    assert not os.path.exists(os.path.join(os.getcwd(), "imports"))
