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

Integration tests for transfer HTTP data plane: gzip download, checksum mismatch,
resume conflict, session model, and client transport chunk assembly.
"""

from __future__ import annotations

import asyncio
import gzip
import hashlib
import os
import sys
import tempfile
from collections.abc import Callable, Generator
from pathlib import Path

import httpx
import pytest
from fastapi.testclient import TestClient

PROJECT_ROOT = Path(__file__).resolve().parents[1]
MTLS_CLIENT_CERT = PROJECT_ROOT / "mtls_certificates" / "client" / "test-client.crt"
MTLS_CLIENT_KEY = PROJECT_ROOT / "mtls_certificates" / "client" / "test-client.key"
MTLS_CA = PROJECT_ROOT / "mtls_certificates" / "ca" / "ca.crt"
INTEGRATION_MTLS_HOST = "127.0.0.1"
INTEGRATION_MTLS_PORT = 8443
_SERVER_WAIT_TIMEOUT_SEC = 12.0
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from mcp_proxy_adapter.api.app import create_app  # noqa: E402
import mcp_proxy_adapter.api.handlers as handlers  # noqa: E402
from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient  # noqa: E402
from mcp_proxy_adapter.client.jsonrpc_client.transport import (  # noqa: E402
    JsonRpcTransport,
)
from mcp_proxy_adapter.transfer.models import (  # noqa: E402
    TransferSession,
    TransferStatus,
)
from mcp_proxy_adapter.transfer.server_store import TransferServerStore  # noqa: E402


def _minimal_session_kwargs() -> dict[str, object]:
    return {
        "transfer_id": "tr_test",
        "filename": "f.bin",
        "direction": "upload",
        "size_bytes": 100,
        "checksum_value": "a" * 64,
        "chunk_size": 1024,
        "status": TransferStatus.CREATED.value,
        "storage_path": "/tmp/storage",
        "metadata_path": "/tmp/meta.json",
    }


@pytest.fixture
def transfer_tmp_dirs(tmp_path: Path) -> Path:
    root = tmp_path / "troot"
    (root / "sessions").mkdir(parents=True)
    return root


@pytest.fixture
def transfer_client_and_cfg(
    transfer_tmp_dirs: Path, monkeypatch: pytest.MonkeyPatch
) -> Generator[tuple[TestClient, Callable[[], dict[str, object]]], None, None]:
    root = transfer_tmp_dirs
    store_dir = root / "store"
    gzip_buf = root / "gzip_buf"
    store_dir.mkdir(parents=True, exist_ok=True)
    gzip_buf.mkdir(parents=True, exist_ok=True)

    def _cfg() -> dict[str, object]:
        return {
            "storage_dir": str(store_dir),
            "session_ttl_seconds": 86400,
            "max_file_size_bytes": 10_737_418_240,
            "default_chunk_size_bytes": 64,
            "max_chunk_size_bytes": 1024,
            "checksum_algorithm": "sha256",
            "server_gzip_download_buffer_dir": str(gzip_buf),
        }

    monkeypatch.setattr(handlers, "_merge_transfer_config", _cfg)
    handlers._CACHED_TRANSFER_STORE = None
    app = create_app()
    yield TestClient(app), _cfg
    handlers._CACHED_TRANSFER_STORE = None


def test_transfer_session_to_dict_includes_new_fields() -> None:
    base = _minimal_session_kwargs()
    s = TransferSession(
        **base,  # type: ignore[arg-type]
        compressed_storage_path="/gzip/a.gz",
        plaintext_size_bytes=99,
    )
    d = s.to_dict()
    assert d["compressed_storage_path"] == "/gzip/a.gz"
    assert d["plaintext_size_bytes"] == 99


def test_transfer_session_from_dict_legacy_payload_omits_new_fields() -> None:
    s = TransferSession.from_dict(_minimal_session_kwargs())
    assert s.compressed_storage_path is None
    assert s.plaintext_size_bytes is None


def test_gzip_download_happy_path_multichunk_no_content_encoding(
    transfer_client_and_cfg: tuple[TestClient, Callable[[], dict[str, object]]],
) -> None:
    client, cfg = transfer_client_and_cfg
    assert callable(cfg)
    store_dir = Path(str(cfg()["storage_dir"]))
    plaintext = b"hello gzip world " * 20
    src_path = store_dir / "source.bin"
    src_path.write_bytes(plaintext)
    expected_sha = hashlib.sha256(plaintext).hexdigest()

    r = client.post(
        "/api/transfer/downloads",
        json={
            "source_path": str(src_path),
            "filename": "source.bin",
            "compression": "gzip",
            "checksum_algorithm": "sha256",
            "chunk_size": 32,
        },
    )
    assert r.status_code == 201, r.text
    body = r.json()
    assert body["checksum_value"] == expected_sha
    assert body.get("plaintext_size_bytes") == len(plaintext)
    tid = str(body["transfer_id"])
    wire_size = int(body["size_bytes"])
    assert wire_size > 0

    buf = bytearray()
    offset = 0
    limit = 40
    while True:
        resp = client.get(
            f"/api/transfer/downloads/{tid}/chunks",
            params={"offset": offset, "limit": limit},
        )
        assert resp.status_code == 200, resp.text
        assert "content-encoding" not in {k.lower() for k in resp.headers.keys()}
        assert resp.headers.get("content-type") == "application/octet-stream"
        assert resp.headers.get("X-Transfer-Wire-Size-Bytes") == str(wire_size)
        assert resp.headers.get("X-Transfer-Plaintext-Size-Bytes") == str(
            len(plaintext)
        )
        buf.extend(resp.content)
        off_hdr = resp.headers.get("X-Transfer-Offset")
        assert off_hdr is not None
        offset = int(off_hdr)
        if resp.headers.get("X-Transfer-Completed", "").lower() == "true":
            break
    assert len(buf) == wire_size
    plain = gzip.decompress(bytes(buf))
    assert plain == plaintext
    assert hashlib.sha256(plain).hexdigest() == expected_sha


def test_upload_complete_checksum_mismatch_422(
    transfer_client_and_cfg: tuple[TestClient, Callable[[], dict[str, object]]],
) -> None:
    client, _cfg = transfer_client_and_cfg
    wrong_sha = "b" * 64
    payload = b"tiny"
    r0 = client.post(
        "/api/transfer/uploads",
        json={
            "filename": "x.bin",
            "size_bytes": len(payload),
            "checksum_algorithm": "sha256",
            "checksum_value": wrong_sha,
            "compression": "identity",
            "chunk_size": 64,
        },
    )
    assert r0.status_code == 201, r0.text
    tid = str(r0.json()["transfer_id"])
    r1 = client.put(
        f"/api/transfer/uploads/{tid}/chunks?offset=0",
        content=payload,
        headers={"Content-Type": "application/octet-stream"},
    )
    assert r1.status_code == 200, r1.text
    r2 = client.post(f"/api/transfer/uploads/{tid}/complete")
    assert r2.status_code == 422, r2.text
    err = r2.json()
    assert err.get("success") is False
    assert err.get("error_type") == "TransferChecksumMismatchError"


def test_download_resume_conflict_409(
    transfer_client_and_cfg: tuple[TestClient, Callable[[], dict[str, object]]],
) -> None:
    client, cfg = transfer_client_and_cfg
    store_dir = Path(str(cfg()["storage_dir"]))
    plaintext = b"resume conflict test data"
    src_path = store_dir / "rc.bin"
    src_path.write_bytes(plaintext)
    r = client.post(
        "/api/transfer/downloads",
        json={
            "source_path": str(src_path),
            "filename": "rc.bin",
            "compression": "gzip",
        },
    )
    assert r.status_code == 201, r.text
    tid = str(r.json()["transfer_id"])
    r1 = client.get(
        f"/api/transfer/downloads/{tid}/chunks",
        params={"offset": 0, "limit": 5},
    )
    assert r1.status_code == 200, r1.text
    r2 = client.get(
        f"/api/transfer/downloads/{tid}/chunks",
        params={"offset": 0, "limit": 5},
    )
    assert r2.status_code == 409, r2.text
    assert r2.json().get("error_type") == "TransferResumeConflictError"


def test_gzip_download_range_beyond_wire_returns_416(
    transfer_client_and_cfg: tuple[TestClient, Callable[[], dict[str, object]]],
) -> None:
    client, cfg = transfer_client_and_cfg
    store_dir = Path(str(cfg()["storage_dir"]))
    plaintext = b"x" * 50
    src_path = store_dir / "range_gz.bin"
    src_path.write_bytes(plaintext)
    r = client.post(
        "/api/transfer/downloads",
        json={
            "source_path": str(src_path),
            "filename": "range_gz.bin",
            "compression": "gzip",
        },
    )
    assert r.status_code == 201, r.text
    tid = str(r.json()["transfer_id"])
    wire_size = int(r.json()["size_bytes"])
    bad_off = wire_size + 100
    r_bad = client.get(
        f"/api/transfer/downloads/{tid}/chunks",
        params={"offset": bad_off, "limit": 1},
    )
    assert r_bad.status_code == 416, r_bad.text
    err = r_bad.json()
    assert err.get("success") is False
    assert err.get("error_type") == "TransferRangeNotSatisfiableError"


def test_expire_stale_sessions_removes_gzip_artifact(tmp_path: Path) -> None:
    store_root = tmp_path / "s"
    gzip_root = tmp_path / "g"
    store = TransferServerStore(
        storage_dir=str(store_root),
        session_ttl_seconds=60,
        max_file_size_bytes=10_000_000,
        default_chunk_size_bytes=1024,
        max_chunk_size_bytes=8192,
        server_gzip_download_buffer_dir=str(gzip_root),
    )
    src = tmp_path / "plain.txt"
    src.write_bytes(b"expire me")
    rec = store.create_download_session(
        source_path=str(src),
        filename="plain.txt",
        checksum_algorithm="sha256",
        compression="gzip",
    )
    tid = rec.transfer_id
    session = store.get_session(tid)
    artifact_path = session.compressed_storage_path
    assert artifact_path
    assert Path(artifact_path).is_file()
    # force expiry
    session.expires_at = "2000-01-01T00:00:00+00:00"
    store._persist_session(session)  # noqa: SLF001
    result = store.expire_stale_sessions()
    assert result.count >= 1
    assert (tid, "download") in result.expired
    assert not Path(artifact_path).is_file()


@pytest.mark.asyncio
async def test_jsonrpc_transport_gzip_download_wire_and_plaintext_headers(
    transfer_client_and_cfg: tuple[TestClient, Callable[[], dict[str, object]]],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    client_sync, cfg = transfer_client_and_cfg
    store_dir = Path(str(cfg()["storage_dir"]))
    plaintext = b"transport " * 15
    src_path = store_dir / "tr.bin"
    src_path.write_bytes(plaintext)
    r = client_sync.post(
        "/api/transfer/downloads",
        json={
            "source_path": str(src_path),
            "filename": "tr.bin",
            "compression": "gzip",
            "chunk_size": 24,
        },
    )
    assert r.status_code == 201, r.text
    tid = str(r.json()["transfer_id"])
    expected_plain_len = len(plaintext)

    app = client_sync.app
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as ac:

        async def _get_client() -> httpx.AsyncClient:
            return ac

        jr = JsonRpcTransport(host="test", port=80)
        monkeypatch.setattr(jr, "_get_client", _get_client)

        wire, meta = await jr.download_all_chunks_wire(tid, limit=30)
        assert meta.get("plaintext_size_bytes") == expected_plain_len
        assert meta.get("wire_size_bytes") == len(wire)
        assert gzip.decompress(wire) == plaintext
        await jr.close()


async def _wait_mtls_https_health(host: str, port: int, timeout: float) -> bool:
    deadline = asyncio.get_event_loop().time() + timeout
    while asyncio.get_event_loop().time() < deadline:
        try:
            r = httpx.get(
                f"https://{host}:{port}/health",
                verify=False,
                timeout=2.0,
            )
            if r.status_code == 200:
                return True
        except Exception:
            pass
        await asyncio.sleep(0.5)
    return False


@pytest.mark.asyncio
async def test_integration_https_stack_8443_reachable_or_skip(
    integration_test_servers: None,
) -> None:
    if not await _wait_mtls_https_health(
        INTEGRATION_MTLS_HOST, INTEGRATION_MTLS_PORT, _SERVER_WAIT_TIMEOUT_SEC
    ):
        pytest.skip(
            "mTLS/TLS server not available on 127.0.0.1:8443 "
            "(need integration_test_servers)"
        )


@pytest.mark.asyncio
async def test_integration_mtls_identity_download_chunk_or_skip(
    integration_test_servers: None,
) -> None:
    if not MTLS_CLIENT_CERT.exists() or not MTLS_CLIENT_KEY.exists():
        pytest.skip("mTLS certificates not found (mtls_certificates/)")
    if not await _wait_mtls_https_health(
        INTEGRATION_MTLS_HOST, INTEGRATION_MTLS_PORT, _SERVER_WAIT_TIMEOUT_SEC
    ):
        pytest.skip(
            "mTLS server not available on 127.0.0.1:8443 "
            "(need integration_test_servers)"
        )

    plaintext = b"mtls identity download\n"
    tmp = tempfile.NamedTemporaryFile(
        delete=False, dir=str(PROJECT_ROOT), suffix=".mtls_dl.bin"
    )
    tmp.write(plaintext)
    tmp.close()
    source_path = tmp.name
    client = JsonRpcClient(
        protocol="mtls",
        host=INTEGRATION_MTLS_HOST,
        port=INTEGRATION_MTLS_PORT,
        cert=str(MTLS_CLIENT_CERT),
        key=str(MTLS_CLIENT_KEY),
        ca=str(MTLS_CA) if MTLS_CA.exists() else None,
        check_hostname=False,
        token_header="X-API-Key",
        token="admin-secret-key-mtls",
    )
    try:
        receipt = await client.create_download_session(
            source_path=source_path,
            filename="mtls.bin",
            compression="identity",
        )
        transfer_id = str(receipt["transfer_id"])
        chunk, meta = await client.download_chunk(
            transfer_id, 0, min(1024, len(plaintext))
        )
        full = Path(source_path).read_bytes()
        assert chunk == full[: len(chunk)]
        completed = meta["completed"]
        assert isinstance(completed, bool)
        if len(full) <= len(chunk):
            assert completed is True
        else:
            assert len(chunk) > 0
            off = meta["offset"]
            assert isinstance(off, int)
    finally:
        await client.close()
        try:
            os.unlink(source_path)
        except OSError:
            pass
