"""start_build attachments: bounds, prompt assembly, and credit-estimate wiring."""

from datetime import timedelta
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from pydantic import ValidationError
from src.core.exceptions import ValidationError as CoreValidationError
from src.domain.agent_access.operations import (
    MAX_ATTACHMENT_CHARS,
    MAX_ATTACHMENTS,
    MAX_ATTACHMENTS_TOTAL_CHARS,
    AttachmentInput,
    StartBuild,
)
from src.domain.agent_access.service import utcnow
from src.domain.codegen.agent_facade import AgentCodegenFacade
from src.domain.prompt_media.models import PromptMediaAsset


def _build(**overrides):
    body = {"request_key": "k" * 8, "project_id": uuid4(), "prompt": "Rebuild this landing page"}
    body.update(overrides)
    return StartBuild(**body)


def test_accepts_a_small_attachment_list():
    build = _build(attachments=[{"path": "index.html", "content": "<h1>Hi</h1>"}])
    assert build.attachments[0].path == "index.html"


def test_source_adoption_accepts_the_three_known_modes():
    for mode in ("add", "replace", "merge"):
        assert _build(source_adoption=mode).source_adoption == mode


def test_source_adoption_rejects_unknown_values():
    with pytest.raises(ValidationError):
        _build(source_adoption="delete_everything")


def test_rejects_more_than_the_attachment_count_limit():
    attachments = [{"path": f"f{i}.html", "content": "x"} for i in range(MAX_ATTACHMENTS + 1)]
    with pytest.raises(ValidationError):
        _build(attachments=attachments)


def test_rejects_a_single_attachment_over_the_per_file_limit():
    with pytest.raises(ValidationError):
        _build(attachments=[{"path": "big.html", "content": "x" * (MAX_ATTACHMENT_CHARS + 1)}])


def test_rejects_total_attachment_size_even_when_each_file_is_individually_small():
    # 4 files just under the per-file cap can still exceed the combined-total cap.
    per_file = MAX_ATTACHMENTS_TOTAL_CHARS // 3
    attachments = [{"path": f"f{i}.html", "content": "x" * per_file} for i in range(4)]
    with pytest.raises(ValidationError):
        _build(attachments=attachments)


def test_attachment_input_requires_exactly_one_content_source():
    with pytest.raises(ValidationError):
        AttachmentInput(path="a.css", content="body {}", asset_id=uuid4())
    with pytest.raises(ValidationError):
        AttachmentInput(path="a.css")


def test_attachment_input_accepts_asset_id_alone():
    attachment = AttachmentInput(path="a.css", asset_id=uuid4())
    assert attachment.content is None


def _facade():
    facade = AgentCodegenFacade(MagicMock())
    facade.projects.get_by_id = AsyncMock(return_value=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))


async def test_attachments_are_appended_to_the_prompt_sent_to_codegen():
    facade = _facade()
    body = _build(attachments=[{"path": "index.html", "content": "<h1>Exact design</h1>"}])
    await facade.start(_grant(), body)
    kwargs = facade.service.create_run.await_args.kwargs
    assert "Rebuild this landing page" in kwargs["user_prompt"]
    assert "<h1>Exact design</h1>" in kwargs["user_prompt"]
    assert "index.html" in kwargs["user_prompt"]
    assert "P2HS's existing App Block" in kwargs["user_prompt"]


async def test_prompt_states_priority_order_and_third_party_preservation():
    facade = _facade()
    body = _build(attachments=[{"path": "index.html", "content": "<h1>Exact design</h1>"}])
    await facade.start(_grant(), body)
    prompt = facade.service.create_run.await_args.kwargs["user_prompt"]
    assert "P2HS security/tenancy/platform invariants" in prompt
    assert "must be preserved, not deleted" in prompt
    assert "Never preserve a hardcoded secret" in prompt


async def test_source_adoption_mode_is_stated_when_given():
    facade = _facade()
    body = _build(
        attachments=[{"path": "index.html", "content": "<h1>Exact design</h1>"}],
        source_adoption="replace",
    )
    await facade.start(_grant(), body)
    prompt = facade.service.create_run.await_args.kwargs["user_prompt"]
    assert "Replace the relevant existing surface with this." in prompt


async def test_source_adoption_defaults_to_unspecified():
    facade = _facade()
    body = _build(attachments=[{"path": "index.html", "content": "<h1>Exact design</h1>"}])
    await facade.start(_grant(), body)
    prompt = facade.service.create_run.await_args.kwargs["user_prompt"]
    assert "Adoption mode not specified" in prompt


async def test_no_attachments_leaves_the_prompt_unchanged():
    facade = _facade()
    body = _build()
    await facade.start(_grant(), body)
    kwargs = facade.service.create_run.await_args.kwargs
    assert kwargs["user_prompt"] == "Rebuild this landing page"


async def test_attachment_content_containing_a_fence_does_not_break_out_of_its_block():
    facade = _facade()
    tricky = "before\n```\nnested fence\n```\nafter"
    body = _build(attachments=[{"path": "snippet.md", "content": tricky}])
    await facade.start(_grant(), body)
    prompt = facade.service.create_run.await_args.kwargs["user_prompt"]
    # A wider fence (````) must wrap content that itself contains ``` fences.
    assert "````\n" + tricky + "\n````" in prompt


async def test_credit_estimate_uses_attachment_sizes_not_prompt_length():
    facade = _facade()
    body = _build(attachments=[{"path": "a.html", "content": "x" * 50_000}])
    await facade.start(_grant(), body)
    kwargs = facade.service.create_run.await_args.kwargs
    # Byte-based attachment accounting (~1 credit for a 50KB file), not the
    # char-count prompt formula, which would wildly overcount a pasted design.
    assert kwargs["credit_estimate"]["attachment_credits"] == 1


async def test_source_adoption_plan_is_durable_and_secrets_are_redacted_before_codegen():
    facade = _facade()
    body = _build(
        attachments=[
            {
                "path": "App.tsx",
                "content": "const stripe = 'sk_live_123456789'; fetch('https://erp.example.com/inventory')",
            }
        ],
        source_adoption="replace",
        deployment_intent="modify_and_deploy",
    )
    await facade.start(_grant(), body)
    kwargs = facade.service.create_run.await_args.kwargs
    assert kwargs["generation_request"]["schema"] == "source_adoption_request.v1"
    assert (
        kwargs["generation_request"]["source_adoption_plan"]["deployment_intent"]
        == "modify_and_deploy"
    )
    assert any(
        item["name"] == "erp.example.com"
        for item in kwargs["generation_request"]["source_adoption_plan"]["external_bindings"]
    )
    assert "<P2HS_SECRET_REDACTED>" in kwargs["user_prompt"]
    assert "sk_live_123456789" not in kwargs["user_prompt"]


def _uploaded_asset(**overrides):
    defaults = dict(
        id=uuid4(),
        project_id=uuid4(),
        uploaded_by=None,
        kind="file",
        storage_relative_path="rel/styles.css",
        original_filename="styles.css",
        mime_type="text/css",
        byte_size=21,
        transcript=None,
        created_at=utcnow(),
    )
    defaults.update(overrides)
    return PromptMediaAsset(**defaults)


def _write_upload(upload_root, rel_path, data):
    import os

    abs_path = os.path.join(upload_root, *rel_path.split("/"))
    os.makedirs(os.path.dirname(abs_path), exist_ok=True)
    mode = "wb" if isinstance(data, bytes) else "w"
    kwargs = {} if isinstance(data, bytes) else {"encoding": "utf-8"}
    with open(abs_path, mode, **kwargs) as f:
        f.write(data)


async def _with_upload_root(assets, run):
    """Run `run(upload_root)` with agent_facade's PromptMediaRepository and
    get_settings patched to a real (temp-dir-backed) upload root, so asset_id
    attachments can be read from disk exactly like confirm_asset_upload writes
    them, instead of retyping content inline."""
    import shutil
    import tempfile

    upload_root = tempfile.mkdtemp(prefix="p2hs-agent-attachments-test-")
    try:
        with patch("src.domain.codegen.agent_facade.PromptMediaRepository") as repo_cls, patch(
            "src.domain.codegen.agent_facade.get_settings",
            return_value=SimpleNamespace(
                prompt_media_upload_root=upload_root,
                agent_asset_upload_total_bytes=300 * 1024 * 1024,
            ),
        ):
            repo_cls.return_value = SimpleNamespace(
                list_by_ids_for_project=AsyncMock(return_value=assets)
            )
            await run(upload_root)
    finally:
        shutil.rmtree(upload_root, ignore_errors=True)


async def test_asset_id_attachment_is_read_from_disk_instead_of_retyped():
    facade = _facade()
    asset = _uploaded_asset()

    async def run(upload_root):
        _write_upload(upload_root, asset.storage_relative_path, "body { color: red; }")
        body = _build(attachments=[{"path": "styles.css", "asset_id": asset.id}])
        await facade.start(_grant(), body)

    await _with_upload_root([asset], run)
    prompt = facade.service.create_run.await_args.kwargs["user_prompt"]
    assert "body { color: red; }" in prompt
    assert "styles.css" in prompt


async def test_asset_id_attachment_rejects_non_utf8_content():
    facade = _facade()
    asset = _uploaded_asset()

    async def run(upload_root):
        _write_upload(upload_root, asset.storage_relative_path, b"\xff\xfe\x00binary")
        body = _build(attachments=[{"path": "styles.css", "asset_id": asset.id}])
        with pytest.raises(CoreValidationError):
            await facade.start(_grant(), body)

    await _with_upload_root([asset], run)


async def test_asset_id_attachment_rejects_asset_belonging_to_another_project():
    facade = _facade()
    asset = _uploaded_asset()

    async def run(upload_root):
        body = _build(attachments=[{"path": "styles.css", "asset_id": asset.id}])
        with pytest.raises(CoreValidationError):
            await facade.start(_grant(), body)

    # list_by_ids_for_project returning [] simulates "not found for this project".
    await _with_upload_root([], run)


async def test_combined_total_across_inline_and_asset_attachments_is_enforced():
    # Each file individually sits at the per-file cap (passes both the schema's
    # Field(max_length=...) on inline content and the facade's per-file check on
    # resolved asset text), but four inline + three asset-backed files together
    # exceed MAX_ATTACHMENTS_TOTAL_CHARS -- a combination only the facade (which
    # sees the resolved asset text) can catch; the schema-level validator only
    # sums inline content and would let this through on its own.
    facade = _facade()
    assets = [_uploaded_asset(byte_size=MAX_ATTACHMENT_CHARS) for _ in range(3)]

    async def run(upload_root):
        for i, asset in enumerate(assets):
            asset.storage_relative_path = f"rel/asset{i}.css"
            _write_upload(upload_root, asset.storage_relative_path, "x" * MAX_ATTACHMENT_CHARS)
        body = _build(
            attachments=[
                *[{"path": f"inline{i}.html", "content": "x" * MAX_ATTACHMENT_CHARS} for i in range(4)],
                *[{"path": f"asset{i}.css", "asset_id": asset.id} for i, asset in enumerate(assets)],
            ]
        )
        with pytest.raises(CoreValidationError):
            await facade.start(_grant(), body)

    await _with_upload_root(assets, run)
