"""
Resumable transfer (G4) acceptance tests mapped to parent spec AC1–AC8.

Uses in-process TestClient where possible; integration stack (8080/8443/3005)
for transport parity and end-to-end transfer I/O.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

import json
import socket
import ssl
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from mcp_proxy_adapter.api.app import create_app
from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
from mcp_proxy_adapter.client.jsonrpc_client.ws_job_status import (
    open_bidirectional_ws_channel,
)
from mcp_proxy_adapter.commands.command_registry import registry
from mcp_proxy_adapter.commands.transfer_download_begin_command import (
    TransferDownloadBeginCommand,
)
from mcp_proxy_adapter.commands.transfer_download_status_command import (
    TransferDownloadStatusCommand,
)
from mcp_proxy_adapter.commands.transfer_upload_begin_command import (
    TransferUploadBeginCommand,
)
from mcp_proxy_adapter.commands.transfer_upload_complete_command import (
    TransferUploadCompleteCommand,
)
from mcp_proxy_adapter.commands.transfer_upload_status_command import (
    TransferUploadStatusCommand,
)
from mcp_proxy_adapter.core.job_push.events import (
    ACTION_SUBSCRIBE_ERROR,
    EVENT_TRANSFER_ACCEPTED,
    EVENT_TRANSFER_COMPLETED,
    EVENT_TRANSFER_EXPIRED,
    EVENT_TRANSFER_FAILED,
    EVENT_TRANSFER_PROGRESS,
    SUBSCRIPTION_TYPE_TRANSFER_SESSION,
)
from mcp_proxy_adapter.transfer.checksums import verify_file_checksum
from mcp_proxy_adapter.transfer.errors import TransferChecksumMismatchError
from mcp_proxy_adapter.transfer.progress_percent import (
    compute_transfer_progress_percent,
)

# --- AC1: lifecycle commands registered with discoverable metadata ---

TRANSFER_COMMAND_NAMES = frozenset(
    {
        "transfer_upload_begin",
        "transfer_upload_status",
        "transfer_upload_complete",
        "transfer_download_begin",
        "transfer_download_status",
    }
)

_TRANSFER_COMMAND_CLASSES = (
    TransferUploadBeginCommand,
    TransferUploadStatusCommand,
    TransferUploadCompleteCommand,
    TransferDownloadBeginCommand,
    TransferDownloadStatusCommand,
)


@pytest.fixture
def transfer_test_client() -> TestClient:
    """App with built-in commands and routes (same stack as help integration tests)."""
    app = create_app(
        title="Transfer G4 Test",
        description="Transfer acceptance",
        version="1.0.0",
    )
    return TestClient(app)


def _help_list_command_names(client: TestClient) -> set[str]:
    response = client.post(
        "/api/jsonrpc",
        json={"jsonrpc": "2.0", "method": "help", "params": {}, "id": 1},
    )
    assert response.status_code == 200
    data = response.json()
    assert data.get("result", {}).get("success") is True
    commands = data["result"]["data"]["commands"]
    assert isinstance(commands, dict)
    return set(commands.keys())


def test_ac1_transfer_commands_listed_in_help_jsonrpc(
    transfer_test_client: TestClient,
) -> None:
    """AC1: transfer_* commands appear in help listing like other builtins."""
    names = _help_list_command_names(transfer_test_client)
    assert TRANSFER_COMMAND_NAMES <= names


@pytest.mark.parametrize("cmd", sorted(TRANSFER_COMMAND_NAMES))
def test_ac1_transfer_command_help_includes_schema_and_examples(
    transfer_test_client: TestClient, cmd: str
) -> None:
    """AC1: each transfer command has detailed help with schema and examples."""
    response = transfer_test_client.post(
        "/api/jsonrpc",
        json={
            "jsonrpc": "2.0",
            "method": "help",
            "params": {"cmdname": cmd},
            "id": 2,
        },
    )
    assert response.status_code == 200
    payload = response.json()
    assert payload.get("result", {}).get("success") is True
    inner = payload["result"]["data"]
    schema = inner.get("schema", {})
    assert isinstance(schema, dict)
    assert schema.get("type") == "object"
    assert "properties" in schema
    examples = schema.get("examples", [])
    assert isinstance(examples, list)
    assert len(examples) >= 1


# --- AC2: discovery parity — health, OpenAPI, help ---


def test_ac2_health_openapi_and_help_expose_transfer_surface(
    transfer_test_client: TestClient,
) -> None:
    """AC2: GET /health succeeds; OpenAPI lists transfer routes; help lists transfer commands."""
    h = transfer_test_client.get("/health")
    assert h.status_code == 200
    body = h.json()
    assert body.get("status") == "ok"

    spec = transfer_test_client.get("/openapi.json")
    assert spec.status_code == 200
    openapi = spec.json()
    paths = openapi.get("paths") or {}
    path_keys = "".join(paths.keys())
    assert "/api/transfer/" in path_keys or "/api/transfer/uploads" in path_keys
    assert "/api/jsonrpc" in paths

    names = _help_list_command_names(transfer_test_client)
    assert "health" in names
    assert TRANSFER_COMMAND_NAMES <= names


# --- AC3: transport parity (integration stack) ---


def _port_open(host: str, port: int, timeout: float = 0.5) -> bool:
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.settimeout(timeout)
            sock.connect((host, port))
        return True
    except OSError:
        return False


@pytest.mark.asyncio
@pytest.mark.integration
async def test_ac3_http_jsonrpc_help_and_transfer_route_reachable(
    integration_test_servers: None,
) -> None:
    """AC3: HTTP stack — JSON-RPC help lists transfer commands; transfer API responds."""
    if not _port_open("127.0.0.1", 8080):
        pytest.skip("integration HTTP server not on 8080")

    client = JsonRpcClient(protocol="http", host="127.0.0.1", port=8080)
    try:
        health = await client.health()
        assert health.get("status") == "ok"

        raw = await client.jsonrpc_call("help", {})
        data = raw.get("result", {}).get("data", {})
        commands = data.get("commands", {})
        if isinstance(commands, dict):
            assert TRANSFER_COMMAND_NAMES <= set(commands.keys())

        import httpx

        r = httpx.get(
            "http://127.0.0.1:8080/api/transfer/uploads/not-a-real-id", timeout=5.0
        )
        assert r.status_code in (400, 404, 422)
    finally:
        await client.close()


@pytest.mark.asyncio
@pytest.mark.integration
async def test_ac3_mtls_help_and_transfer_route_reachable(
    integration_test_servers: None,
) -> None:
    """AC3: mTLS stack — same discovery and transfer HTTP surface on 8443."""
    project_root = Path(__file__).resolve().parents[1]
    cert = project_root / "mtls_certificates" / "client" / "test-client.crt"
    key = project_root / "mtls_certificates" / "client" / "test-client.key"
    ca = project_root / "mtls_certificates" / "ca" / "ca.crt"
    if not cert.exists() or not key.exists() or not ca.exists():
        pytest.skip("mTLS certificates not found under mtls_certificates/")

    if not _port_open("127.0.0.1", 8443):
        pytest.skip("integration mTLS server not on 8443")

    client = JsonRpcClient(
        protocol="mtls",
        host="127.0.0.1",
        port=8443,
        cert=str(cert),
        key=str(key),
        ca=str(ca),
        check_hostname=False,
        token_header="X-API-Key",
        token="admin-secret-key-mtls",
    )
    try:
        health = await client.health()
        assert health.get("status") == "ok"

        raw = await client.jsonrpc_call("help", {})
        data = raw.get("result", {}).get("data", {})
        commands = data.get("commands", {})
        if isinstance(commands, dict):
            assert TRANSFER_COMMAND_NAMES <= set(commands.keys())

        import httpx

        ssl_ctx = ssl.create_default_context(cafile=str(ca))
        ssl_ctx.check_hostname = False

        with httpx.Client(
            cert=(str(cert), str(key)),
            verify=ssl_ctx,
            headers={"X-API-Key": "admin-secret-key-mtls"},
            timeout=10.0,
        ) as http:
            r = http.get(
                "https://127.0.0.1:8443/api/transfer/uploads/not-a-real-id",
            )
        assert r.status_code in (400, 404, 422)
    finally:
        await client.close()


@pytest.mark.integration
def test_ac3_proxy_listen_on_configured_port() -> None:
    """AC3: integration proxy (3005) accepts TCP when conftest starts it."""
    if not _port_open("127.0.0.1", 3005):
        pytest.skip("Proxy not listening (optional run_proxy_server missing or failed)")


# --- AC4: WebSocket transfer_session channel (errors only; no full push) ---


@pytest.mark.asyncio
@pytest.mark.integration
async def test_ac4_ws_transfer_session_subscribe_unknown_id_errors(
    integration_test_servers: None,
) -> None:
    """AC4: /ws accepts transfer_session subscribe; unknown transfer_id yields error."""
    if not _port_open("127.0.0.1", 8080):
        pytest.skip("integration HTTP server not on 8080")

    client = JsonRpcClient(protocol="http", host="127.0.0.1", port=8080)
    try:
        async with open_bidirectional_ws_channel(client, receive_timeout=15.0) as ch:
            await ch.send_json(
                {
                    "action": "subscribe",
                    "subscription_type": SUBSCRIPTION_TYPE_TRANSFER_SESSION,
                    "transfer_id": "00000000-0000-0000-0000-000000000001",
                }
            )
            seen = False
            async for msg in ch.receive_iter():
                seen = True
                assert msg.get("action") == ACTION_SUBSCRIBE_ERROR
                err = str(msg.get("error", "")).lower()
                assert "not found" in err or "unknown" in err or "transfer" in err
                break
            assert seen, "expected subscribe_error from server"
    finally:
        await client.close()


def test_ac4_transfer_event_constants_are_transfer_scoped() -> None:
    """AC4: transfer WS event names are distinct transfer_* identifiers."""
    names = {
        EVENT_TRANSFER_ACCEPTED,
        EVENT_TRANSFER_PROGRESS,
        EVENT_TRANSFER_COMPLETED,
        EVENT_TRANSFER_FAILED,
        EVENT_TRANSFER_EXPIRED,
    }
    for n in names:
        assert n.startswith("transfer_"), n


# --- AC5: progress_percent 0–100 vs offset/size_bytes ---


def test_ac5_progress_percent_matches_offset_and_size() -> None:
    """AC5: progress_percent is in 0..100 and tracks offset/size_bytes."""
    assert compute_transfer_progress_percent(0, 100, terminal=False) == 0
    assert compute_transfer_progress_percent(50, 100, terminal=False) == 50
    assert compute_transfer_progress_percent(100, 100, terminal=False) == 100
    assert compute_transfer_progress_percent(0, 0, terminal=True) == 100
    assert compute_transfer_progress_percent(1000, 100, terminal=False) == 100


# --- AC6: registry schema matches command class schema ---


def test_ac6_registry_schema_matches_command_class_for_transfer_commands(
    transfer_test_client: TestClient,
) -> None:
    """AC6: command_registry exposes the same schema as Command.get_schema()."""
    _ = transfer_test_client
    for cls in _TRANSFER_COMMAND_CLASSES:
        name = getattr(cls, "name", None)
        assert isinstance(name, str)
        info = registry.get_command_info(name)
        assert info is not None, name
        reg_schema = info.get("schema")
        class_schema = cls.get_schema()
        assert reg_schema == class_schema


# --- AC7: gzip + single-stream documentation path ---


def test_ac7_upload_schema_allows_identity_and_gzip() -> None:
    """AC7: upload begin schema exposes gzip alongside identity (single-stream staging)."""
    schema = TransferUploadBeginCommand.get_schema()
    props = schema["properties"]
    comp = props["compression"]
    assert "gzip" in json.dumps(comp)


def test_ac7_download_all_chunks_wire_docstring_single_gzip_stream() -> None:
    """AC7: client transport documents concatenated gzip as one stream (decompress once)."""
    from mcp_proxy_adapter.client.jsonrpc_client.transport import JsonRpcTransport

    doc = (JsonRpcTransport.download_all_chunks_wire.__doc__ or "").lower()
    assert "gzip" in doc
    assert "concaten" in doc or "stream" in doc


# --- AC8: checksum verification on client ---


def test_ac8_verify_file_checksum_mismatch_raises(tmp_path: Path) -> None:
    """AC8: client-side verify_file_checksum rejects wrong digest (post-receive path)."""
    p = tmp_path / "blob.bin"
    p.write_bytes(b"payload")
    with pytest.raises(TransferChecksumMismatchError):
        verify_file_checksum(
            str(p),
            "0" * 64,
            transfer_id="tid",
            phase="verify",
        )


@pytest.mark.asyncio
@pytest.mark.integration
async def test_ac8_upload_download_roundtrip_client_verifies_checksum(
    integration_test_servers: None,
    tmp_path: Path,
) -> None:
    """AC8: upload then download; downloader verifies SHA-256 after receive."""
    if not _port_open("127.0.0.1", 8080):
        pytest.skip("integration HTTP server not on 8080")

    src = tmp_path / "src.bin"
    src.write_bytes(b"roundtrip-bytes-g4")
    dest = tmp_path / "dest.bin"

    client = JsonRpcClient(protocol="http", host="127.0.0.1", port=8080)
    try:
        receipt = await client.upload_file(str(src), chunk_size=4096)
        assert receipt.completed is True
        assert receipt.checksum_value

        sess = await client.get_upload_session(receipt.transfer_id)
        storage = str(sess["storage_path"])
        fname = str(sess["filename"])

        down_sess = await client.create_download_session(
            source_path=storage,
            filename=fname,
            compression="identity",
            chunk_size=4096,
        )
        down_id = str(down_sess["transfer_id"])
        dl = await client.download_file(down_id, str(dest))
        assert dl.checksum_verified is True
        assert dest.read_bytes() == src.read_bytes()
    finally:
        await client.close()


@pytest.mark.asyncio
@pytest.mark.integration
async def test_ac8_mtls_upload_small_file(
    integration_test_servers: None,
    tmp_path: Path,
) -> None:
    """AC8: mTLS upload completes; receipt carries checksum metadata."""
    project_root = Path(__file__).resolve().parents[1]
    cert = project_root / "mtls_certificates" / "client" / "test-client.crt"
    key = project_root / "mtls_certificates" / "client" / "test-client.key"
    ca = project_root / "mtls_certificates" / "ca" / "ca.crt"
    if not cert.exists() or not key.exists() or not ca.exists():
        pytest.skip("mTLS certificates not found")

    if not _port_open("127.0.0.1", 8443):
        pytest.skip("mTLS server not on 8443")

    src = tmp_path / "mtls.bin"
    src.write_bytes(b"mtls-upload")

    client = JsonRpcClient(
        protocol="mtls",
        host="127.0.0.1",
        port=8443,
        cert=str(cert),
        key=str(key),
        ca=str(ca),
        check_hostname=False,
        token_header="X-API-Key",
        token="admin-secret-key-mtls",
    )
    try:
        receipt = await client.upload_file(str(src), chunk_size=2048)
        assert receipt.completed is True
        assert len(receipt.checksum_value) == 64
    finally:
        await client.close()
