"""Storing a dashboard upload, starting a build from it, and listing what was received (source_import_service.py).

Uses a real temporary directory for the snapshot store (pytest's tmp_path hits a Windows permission problem here)."""

from __future__ import annotations

import os
import tempfile
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest
from src.core.exceptions import NotFoundError, ValidationError
from src.domain.codegen.import_snapshot import read_snapshot_files, snapshots_dir
from src.domain.codegen.source_import_service import (
    DEFAULT_PROMPTS,
    SourceImportStartBody,
    list_imports,
    load_manifest,
    start_from_import,
    store_upload,
)

SITE = [("index.html", b"<title>x</title><h1>Hi</h1>"), ("assets/css/site.css", b"h1{color:red}"),
        ("assets/js/app.js", b"var a = 1")]
SECRET = "sk_live_abcdefghijklmnop123456"


@pytest.fixture
def env():
    with tempfile.TemporaryDirectory() as root:
        settings = SimpleNamespace(repo_workspace=root)
        project = SimpleNamespace(id=uuid4(), workspace_id=uuid4())
        user = SimpleNamespace(id=uuid4())
        yield SimpleNamespace(settings=settings, project=project, user=user)


# ---- storing ---------------------------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_an_upload_is_stored_as_an_import_snapshot_and_described(env):
    described = await store_upload(env.settings, env.project, env.user, SITE)
    assert described["source"] == "dashboard" and described["has_ui_markup"] is True
    assert [f["path"] for f in described["files"]] == ["index.html", "assets/css/site.css", "assets/js/app.js"]
    manifest = load_manifest(env.settings, env.project, described["import_id"])
    assert manifest["source"] == {"kind": "dashboard", "user_id": str(env.user.id)}
    files = read_snapshot_files(snapshots_dir(env.settings, env.project.workspace_id, env.project.id), manifest)
    assert files["index.html"] == b"<title>x</title><h1>Hi</h1>"  # byte for byte what arrived


@pytest.mark.asyncio
async def test_secrets_are_redacted_before_they_are_stored_and_the_person_is_told(env):
    described = await store_upload(env.settings, env.project, env.user,
                                   [("index.html", f"<script>var k='{SECRET}'</script>".encode())])
    manifest = load_manifest(env.settings, env.project, described["import_id"])
    stored = read_snapshot_files(snapshots_dir(env.settings, env.project.workspace_id, env.project.id), manifest)
    assert SECRET.encode() not in stored["index.html"] and b"P2HS_SECRET_REDACTED" in stored["index.html"]
    assert described["files"][0]["redacted"] is True and any("Project Variables" in w for w in described["warnings"])


@pytest.mark.asyncio
async def test_left_out_files_are_reported_back(env):
    described = await store_upload(env.settings, env.project, env.user, SITE + [("logo.png", b"\x89PNG")])
    assert {s["path"] for s in described["skipped"]} == {"logo.png"}


@pytest.mark.asyncio
async def test_an_upload_with_nothing_importable_stores_nothing(env):
    with pytest.raises(ValidationError):
        await store_upload(env.settings, env.project, env.user, [("logo.png", b"\x89PNG")])
    assert not os.path.exists(snapshots_dir(env.settings, env.project.workspace_id, env.project.id))


# ---- isolation ----------------------------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_an_import_is_only_reachable_through_its_own_project(env):
    described = await store_upload(env.settings, env.project, env.user, SITE)
    other = SimpleNamespace(id=uuid4(), workspace_id=env.project.workspace_id)
    with pytest.raises(NotFoundError):
        load_manifest(env.settings, other, described["import_id"])


@pytest.mark.parametrize("bad", ["../../etc", "A" * 32, "abc", "", "z" * 32, "a" * 31 + "/", "a" * 32 + "x"])
def test_an_import_id_that_is_not_a_snapshot_id_is_never_read(env, bad):
    with pytest.raises(NotFoundError):
        load_manifest(env.settings, env.project, bad)


# ---- starting a build ---------------------------------------------------------------------------------------


class FakeFacade:
    calls: list = []

    def __init__(self, db):
        pass

    async def start(self, actor, body, *, prompt_source=None, attachments=None, import_snapshot=None):
        FakeFacade.calls.append(SimpleNamespace(actor=actor, body=body, prompt_source=prompt_source,
                                                attachments=attachments, import_snapshot=import_snapshot))
        return SimpleNamespace(id=uuid4(), status="queued")


@pytest.fixture
def facade():
    FakeFacade.calls = []
    with patch("src.domain.codegen.agent_facade.AgentCodegenFacade", FakeFacade):
        yield FakeFacade


async def start(env, import_id, **body):
    payload = {"source_adoption": "migrate", **body}
    return await start_from_import(object(), env.settings, env.project, env.user, import_id,
                                   SourceImportStartBody(**payload))


@pytest.mark.asyncio
async def test_starting_goes_through_the_same_path_as_an_ai_connection_as_a_person(env, facade):
    described = await store_upload(env.settings, env.project, env.user, SITE)
    run = await start(env, described["import_id"], prompt="Make it work")
    [call] = facade.calls
    assert run.status == "queued"
    assert call.prompt_source == "dashboard_import"
    assert call.actor.id is None and call.actor.created_by == env.user.id  # a person, no AI grant
    assert call.body.prompt == "Make it work" and call.body.source_adoption == "migrate"
    assert call.body.deployment_intent == "modify_and_deploy"
    assert {a.path for a in call.attachments} == {"index.html", "assets/css/site.css", "assets/js/app.js"}
    assert call.import_snapshot["import_id"] == described["import_id"]  # the stored snapshot is reused, not rewritten


@pytest.mark.asyncio
@pytest.mark.parametrize("mode", ["migrate", "replace", "merge", "add"])
async def test_each_mode_has_a_default_instruction_when_the_person_writes_none(env, facade, mode):
    described = await store_upload(env.settings, env.project, env.user, SITE)
    await start(env, described["import_id"], source_adoption=mode)
    assert facade.calls[0].body.prompt == DEFAULT_PROMPTS[mode]
    await start(env, described["import_id"], source_adoption=mode, prompt="   ")
    assert facade.calls[1].body.prompt == DEFAULT_PROMPTS[mode]  # blank counts as none


@pytest.mark.asyncio
async def test_a_migrate_without_any_page_is_refused_with_what_to_send(env, facade):
    described = await store_upload(env.settings, env.project, env.user,
                                   [("data.js", b"window.D = {}"), ("site.css", b"a{}")])
    with pytest.raises(ValidationError) as caught:
        await start(env, described["import_id"])
    assert caught.value.public_error == "ui_source_required" and "cannot open links" in caught.value.message
    assert facade.calls == []  # nothing was started


@pytest.mark.asyncio
async def test_a_data_only_import_can_still_be_merged_or_added(env, facade):
    described = await store_upload(env.settings, env.project, env.user, [("data.js", b"window.D = {}")])
    await start(env, described["import_id"], source_adoption="merge")
    assert len(facade.calls) == 1


@pytest.mark.asyncio
async def test_a_snapshot_that_changed_on_disk_is_not_built_from(env, facade):
    described = await store_upload(env.settings, env.project, env.user, SITE)
    path = snapshots_dir(env.settings, env.project.workspace_id, env.project.id) / described["import_id"]
    (path / "files" / "index.html").write_bytes(b"<h1>tampered</h1>")
    with pytest.raises(ValidationError, match="Upload them again"):
        await start(env, described["import_id"])
    assert facade.calls == []


@pytest.mark.asyncio
async def test_an_unknown_import_is_not_found(env, facade):
    with pytest.raises(NotFoundError):
        await start(env, "f" * 32)


def test_the_start_body_only_accepts_the_four_modes_and_bounds_the_prompt():
    with pytest.raises(ValueError):
        SourceImportStartBody(source_adoption="delete_everything")
    with pytest.raises(ValueError):
        SourceImportStartBody(source_adoption="add", prompt="x" * 8001)
    assert SourceImportStartBody(source_adoption="add").deployment_intent == "modify_and_deploy"


# ---- listing ------------------------------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_the_list_shows_what_was_received_newest_first_with_the_builds_that_used_it(env):
    first = await store_upload(env.settings, env.project, env.user, [("index.html", b"<p>one</p>")])
    second = await store_upload(env.settings, env.project, env.user, SITE)
    base = snapshots_dir(env.settings, env.project.workspace_id, env.project.id)
    os.utime(base / first["import_id"] / "manifest.json", (1_000, 1_000))  # older
    run = SimpleNamespace(id=uuid4(), status="completed", created_at=None,
                          plan={"context": {"import_snapshot": {"import_id": second["import_id"]}}})
    unrelated = SimpleNamespace(id=uuid4(), status="failed", created_at=None, plan=None)
    with patch("src.domain.codegen.repository.CodegenRunRepository") as repo:
        repo.return_value.list_for_project = AsyncMock(return_value=[run, unrelated])
        listed = await list_imports(object(), env.settings, env.project)
    assert [i["import_id"] for i in listed] == [second["import_id"], first["import_id"]]
    assert listed[0]["runs"] == [{"run_id": str(run.id), "status": "completed", "created_at": None}]
    assert listed[1]["runs"] == [] and listed[0]["counts"]["markup"] == 1


@pytest.mark.asyncio
async def test_a_project_with_no_imports_lists_nothing(env):
    with patch("src.domain.codegen.repository.CodegenRunRepository") as repo:
        repo.return_value.list_for_project = AsyncMock(return_value=[])
        assert await list_imports(object(), env.settings, env.project) == []


@pytest.mark.asyncio
async def test_a_damaged_manifest_is_skipped_not_fatal(env):
    good = await store_upload(env.settings, env.project, env.user, SITE)
    base = snapshots_dir(env.settings, env.project.workspace_id, env.project.id)
    broken = base / ("b" * 32)
    broken.mkdir(parents=True)
    (broken / "manifest.json").write_text("{not json", "utf-8")
    with patch("src.domain.codegen.repository.CodegenRunRepository") as repo:
        repo.return_value.list_for_project = AsyncMock(return_value=[])
        listed = await list_imports(object(), env.settings, env.project)
    assert [i["import_id"] for i in listed] == [good["import_id"]]


@pytest.mark.asyncio
async def test_other_projects_imports_are_never_listed(env):
    await store_upload(env.settings, env.project, env.user, SITE)
    other = SimpleNamespace(id=uuid4(), workspace_id=env.project.workspace_id)
    with patch("src.domain.codegen.repository.CodegenRunRepository") as repo:
        repo.return_value.list_for_project = AsyncMock(return_value=[])
        assert await list_imports(object(), env.settings, other) == []



def test_a_traversal_id_cannot_reach_a_manifest_that_really_exists_outside_the_imports_folder(env):
    # the id is only ever a snapshot id: without that rule "../secret" would read this file
    meta = snapshots_dir(env.settings, env.project.workspace_id, env.project.id).parent
    (meta / "secret").mkdir(parents=True)
    (meta / "secret" / "manifest.json").write_text('{"files": [], "import_id": "leaked"}', "utf-8")
    with pytest.raises(NotFoundError):
        load_manifest(env.settings, env.project, "../secret")
    with pytest.raises(NotFoundError):
        load_manifest(env.settings, env.project, "..")
