"""W4P static deployment adapter: request sequence, hashing, and fail-closed behaviour.

No network: Cloudflare is an httpx MockTransport, KV is an in-memory dict.
"""

import json
import re
import tempfile
from pathlib import Path
from types import SimpleNamespace

import httpx
import pytest

from src.domain.hosting.adapters.w4p_static import (
    StaticFile,
    W4PStaticDeploymentProvider,
    W4PStaticError,
    asset_hash,
    asset_content_type,
    build_manifest,
    collect_static_files,
    version_script_name,
)

ACCOUNT = "b6152b2d03f9bbcc29e0023a294163d5"
NS = "p2hs-production"
LIVE = "p2hs-prod-abc123"
SHA = "9f8e7d6c5b4a39281716151413121110"


@pytest.fixture
def tmp_path():
    """pytest's own temp folder is access-denied on this machine, so use a plain temp dir."""
    with tempfile.TemporaryDirectory() as d:
        yield Path(d)


def _ok(result):
    return httpx.Response(200, json={"success": True, "errors": [], "result": result})


def _fake_cloudflare(buckets):
    calls = []

    def handler(request: httpx.Request) -> httpx.Response:
        calls.append((request.method, request.url.path, request.headers.get("authorization", "")))
        path = request.url.path
        if path.endswith("/assets-upload-session"):
            return _ok({"jwt": "SESSION-JWT", "buckets": buckets})
        if path.endswith("/workers/assets/upload"):
            return _ok({"jwt": "COMPLETION-JWT"})
        if "/workers/dispatch/namespaces/" in path and path.count("/scripts/") == 1:
            return _ok({"id": LIVE})
        return httpx.Response(404, json={"success": False, "errors": [{"message": "unexpected"}]})

    return httpx.MockTransport(handler), calls


def _site(tmp_path):
    (tmp_path / "index.html").write_text("<h1>hi</h1>", encoding="utf-8")
    (tmp_path / "about").mkdir()
    (tmp_path / "about" / "index.html").write_text("<h1>about</h1>", encoding="utf-8")
    (tmp_path / ".env").write_text("SECRET=1", encoding="utf-8")
    return tmp_path


def test_asset_hash_is_stable_32_hex_and_depends_on_content_and_extension():
    h = asset_hash("/index.html", b"<h1>hi</h1>")
    assert re.fullmatch(r"[0-9a-f]{32}", h)
    assert h == asset_hash("/index.html", b"<h1>hi</h1>")
    assert h != asset_hash("/index.html", b"<h1>bye</h1>")
    assert h != asset_hash("/index.txt", b"<h1>hi</h1>")


def test_collect_skips_hidden_files_and_requires_index(tmp_path):
    files = collect_static_files(_site(tmp_path))
    paths = [f.path for f in files]
    assert "/index.html" in paths and "/about/index.html" in paths
    assert all(not p.startswith("/.") and "/." not in p for p in paths)


def test_collect_fails_closed_without_index_or_when_empty(tmp_path):
    (tmp_path / "style.css").write_text("body{}", encoding="utf-8")
    with pytest.raises(W4PStaticError, match="static_dir_has_no_index_html"):
        collect_static_files(tmp_path)
    empty = tmp_path / "empty"
    empty.mkdir()
    with pytest.raises(W4PStaticError, match="static_dir_empty"):
        collect_static_files(empty)
    with pytest.raises(W4PStaticError, match="static_dir_missing"):
        collect_static_files(tmp_path / "nope")


def test_manifest_maps_every_path_to_its_hash_and_size():
    files = [StaticFile("/index.html", b"abc"), StaticFile("/a.css", b"body{}")]
    manifest, by_hash = build_manifest(files)
    assert manifest["/index.html"] == {"hash": asset_hash("/index.html", b"abc"), "size": 3}
    assert set(by_hash) == {m["hash"] for m in manifest.values()}


def test_version_script_name_is_live_name_plus_short_sha():
    assert version_script_name(LIVE, SHA) == f"{LIVE}-v9f8e7d6c"
    assert version_script_name(LIVE, "") == f"{LIVE}-vunknown"


def test_asset_content_type_preserves_browser_rendering_mime_types():
    assert asset_content_type("/index.html") == "text/html"
    assert asset_content_type("/_nuxt/app.js") == "text/javascript"
    assert asset_content_type("/_nuxt/style.css") == "text/css"
    assert asset_content_type("/logo.png") == "image/png"
    assert asset_content_type("/unknown.bin") == "application/octet-stream"


@pytest.mark.asyncio
async def test_deploy_runs_session_then_batch_then_put_with_completion_token(tmp_path):
    files = collect_static_files(_site(tmp_path))
    manifest, by_hash = build_manifest(files)
    transport, calls = _fake_cloudflare(buckets=list(by_hash))
    from src.domain.hosting.adapters.w4p_static import W4PStaticClient

    client = W4PStaticClient(account_id=ACCOUNT, token="TOKEN", namespace=NS, transport=transport)
    await client.deploy(f"{LIVE}-vSHA", files, "2026-03-25")

    methods_paths = [(m, p) for m, p, _ in calls]
    assert methods_paths[0] == ("POST", f"/client/v4/accounts/{ACCOUNT}/workers/dispatch/namespaces/{NS}/scripts/{LIVE}-vSHA/assets-upload-session")
    assert methods_paths[1] == ("POST", f"/client/v4/accounts/{ACCOUNT}/workers/assets/upload")
    assert methods_paths[2] == ("PUT", f"/client/v4/accounts/{ACCOUNT}/workers/dispatch/namespaces/{NS}/scripts/{LIVE}-vSHA")
    assert all(auth == "Bearer TOKEN" for _, _, auth in calls[:1])
    assert calls[1][2] == "Bearer SESSION-JWT"


@pytest.mark.asyncio
async def test_deploy_skips_upload_when_every_file_is_already_stored(tmp_path):
    files = collect_static_files(_site(tmp_path))
    transport, calls = _fake_cloudflare(buckets=[])
    from src.domain.hosting.adapters.w4p_static import W4PStaticClient

    client = W4PStaticClient(account_id=ACCOUNT, token="TOKEN", namespace=NS, transport=transport)
    await client.deploy(f"{LIVE}-vSHA", files, "2026-03-25")
    assert [m for m, _, _ in calls] == ["POST", "PUT"]


@pytest.mark.asyncio
async def test_put_uses_completion_token_in_assets_metadata(tmp_path):
    files = collect_static_files(_site(tmp_path))
    seen = {}

    def handler(request: httpx.Request) -> httpx.Response:
        if request.method == "PUT":
            seen["body"] = request.read()
        if request.url.path.endswith("/assets-upload-session"):
            return _ok({"jwt": "SESSION-JWT", "buckets": []})
        return _ok({})

    from src.domain.hosting.adapters.w4p_static import W4PStaticClient

    client = W4PStaticClient(account_id=ACCOUNT, token="TOKEN", namespace=NS, transport=httpx.MockTransport(handler))
    await client.deploy(f"{LIVE}-vSHA", files, "2026-03-25")
    assert b'"jwt": "SESSION-JWT"' in seen["body"]
    assert b'"main_module": "worker.js"' in seen["body"]


def _provider(transport, kv):
    async def kv_read(key):
        return kv.get(key)

    async def kv_write(key, value):
        kv[key] = value

    return W4PStaticDeploymentProvider(
        account_id=ACCOUNT, token="TOKEN", namespace=NS, kv_write=kv_write, kv_read=kv_read,
        compatibility_date="2026-03-25", transport=transport,
    )


@pytest.mark.asyncio
async def test_activate_then_rollback_restores_the_previous_routing_record(tmp_path):
    kv = {"host:app.example.com": json.dumps({"script_name": LIVE, "target_kind": "w4p_static"})}
    provider = _provider(httpx.MockTransport(lambda r: _ok({})), kv)
    ctx = SimpleNamespace(project_ref="abc123", git_sha=SHA)
    descriptor = {"target_kind": "w4p_static", "project_ref": "abc123", "environment": "production"}

    out = await provider.activate_deployment(ctx, f"{LIVE}-vSHA", LIVE, "app.example.com", descriptor)
    assert out.ok and out.activation_status == "live"
    assert json.loads(kv["host:app.example.com"])["script_name"] == f"{LIVE}-vSHA"

    back = await provider.rollback("app.example.com")
    assert back.ok and back.activation_status == "rolled_back"
    assert json.loads(kv["host:app.example.com"])["script_name"] == LIVE


@pytest.mark.asyncio
async def test_rollback_fails_closed_without_a_previous_version():
    kv = {"host:app.example.com": json.dumps({"script_name": LIVE})}
    provider = _provider(httpx.MockTransport(lambda r: _ok({})), kv)
    out = await provider.rollback("app.example.com")
    assert out.ok is False and out.message == "no_previous_version"
    assert json.loads(kv["host:app.example.com"])["script_name"] == LIVE


@pytest.mark.asyncio
async def test_deploy_artifact_without_static_dir_fails_closed():
    provider = _provider(httpx.MockTransport(lambda r: _ok({})), {})
    ctx = SimpleNamespace(project_ref="abc123", git_sha=SHA)
    target = SimpleNamespace(environment="production")
    artifact = SimpleNamespace(build_metadata={})
    out = await provider.deploy_artifact(ctx, artifact, target)
    assert out.ok is False and out.message == "static_dir_missing"


@pytest.mark.asyncio
async def test_deploy_artifact_uploads_the_version_script(tmp_path):
    transport, calls = _fake_cloudflare(buckets=[])
    provider = _provider(transport, {})
    ctx = SimpleNamespace(project_ref="abc123", git_sha=SHA)
    target = SimpleNamespace(environment="production")
    artifact = SimpleNamespace(build_metadata={"static_dir": str(_site(tmp_path))})
    out = await provider.deploy_artifact(ctx, artifact, target)
    assert out.ok, out.message
    assert out.provider_resource_id == f"{LIVE}-v9f8e7d6c"
    assert calls[-1][0] == "PUT" and calls[-1][1].endswith(f"/scripts/{LIVE}-v9f8e7d6c")


@pytest.mark.asyncio
async def test_nested_buckets_from_cloudflare_are_uploaded_per_batch(tmp_path):
    """Live spike: the session returns buckets as a list of batches, not a flat list of hashes."""
    files = collect_static_files(_site(tmp_path))
    _, by_hash = build_manifest(files)
    hashes = list(by_hash)
    transport, calls = _fake_cloudflare(buckets=[hashes[:1], hashes[1:]] if len(hashes) > 1 else [hashes])
    from src.domain.hosting.adapters.w4p_static import W4PStaticClient

    client = W4PStaticClient(account_id=ACCOUNT, token="TOKEN", namespace=NS, transport=transport)
    await client.deploy(f"{LIVE}-vSHA", files, "2026-03-25")
    uploads = [p for m, p, _ in calls if p.endswith("/workers/assets/upload")]
    assert len(uploads) == 2 if len(hashes) > 1 else len(uploads) == 1
