"""Tests for the Docstream CLI (spec §6)."""
from __future__ import annotations

import json
from unittest.mock import patch

import httpx
import pytest
from typer.testing import CliRunner

from docstream_cli.main import app
from docstream_cli.transport import parse_source_uri

runner = CliRunner()


# --------------------------------------------------------------------------- transport
class TestTransportParsing:
    def test_local_no_prefix(self) -> None:
        assert parse_source_uri("/path/to/project-docs") == ("local", "/path/to/project-docs")

    def test_local_prefix(self) -> None:
        assert parse_source_uri("local:///path/to/docs") == ("local", "/path/to/docs")

    def test_ssh(self) -> None:
        assert parse_source_uri("ssh://hostA/path/to/docs") == (
            "ssh",
            "hostA:/path/to/docs",
        )

    def test_wireguard(self) -> None:
        assert parse_source_uri("wg://hostB/path/to/docs") == (
            "wireguard",
            "hostB:/path/to/docs",
        )

    def test_ssh_root_path(self) -> None:
        assert parse_source_uri("ssh://hostA/docs") == ("ssh", "hostA:/docs")


# --------------------------------------------------------------------------- HTTP mocking
def _mock_transport(handler) -> httpx.MockTransport:
    return httpx.MockTransport(handler)


def _patch_client_transport(handler) -> object:
    """Patch the SDK's httpx.Client so all requests use a MockTransport.

    The generated ``docstream_api_client`` builds its httpx.Client lazily inside
    ``Client.get_httpx_client()`` via the ``httpx`` reference imported in
    ``docstream_api_client.client``.  Patching that attribute ensures every SDK
    request is routed through the mock transport.
    """

    real_client = httpx.Client

    class _PatchedClient(real_client):
        def __init__(self, *args, **kwargs):
            kwargs.pop("timeout", None)
            super().__init__(*args, transport=_mock_transport(handler), **kwargs)

    return patch("docstream_api_client.client.httpx.Client", _PatchedClient)


# --------------------------------------------------------------------------- create
class TestCreateCommand:
    def test_create_context_basic(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["method"] = request.method
            captured["url"] = str(request.url)
            captured["json"] = json.loads(request.content)
            return httpx.Response(
                201,
                json={"id": "ctx1", "sources": []},
            )

        with _patch_client_transport(handler):
            result = runner.invoke(app, ["create", "--context", "ctx1"])

        assert result.exit_code == 0, result.output
        assert captured["method"] == "POST"
        assert captured["url"].endswith("/contexts/")
        assert captured["json"] == {"id": "ctx1"}

    def test_create_context_with_renderer(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["json"] = json.loads(request.content)
            return httpx.Response(
                201,
                json={
                    "id": "ctx1",
                    "sources": [],
                    "renderer": {"md_engine": "python-markdown", "hl_engine": "pygments"},
                },
            )

        with _patch_client_transport(handler):
            result = runner.invoke(
                app,
                [
                    "create",
                    "--context",
                    "ctx1",
                    "--md-engine",
                    "python-markdown",
                    "--hl-engine",
                    "pygments",
                ],
            )

        assert result.exit_code == 0, result.output
        assert captured["json"]["renderer"] == {
            "md_engine": "python-markdown",
            "hl_engine": "pygments",
        }


# --------------------------------------------------------------------------- add
class TestAddCommand:
    def test_add_local_source(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["method"] = request.method
            captured["url"] = str(request.url)
            captured["json"] = json.loads(request.content)
            return httpx.Response(
                201,
                json={
                    "transport": "local",
                    "location": "/path/to/project-docs",
                    "alias": None,
                    "slug": "project-docs",
                },
            )

        with _patch_client_transport(handler):
            result = runner.invoke(
                app,
                [
                    "add",
                    "--context",
                    "ctx1",
                    "--source",
                    "/path/to/project-docs",
                ],
            )

        assert result.exit_code == 0, result.output
        assert captured["method"] == "POST"
        assert "/contexts/ctx1/sources" in captured["url"]
        assert captured["json"]["transport"] == "local"
        assert captured["json"]["location"] == "/path/to/project-docs"
        # alias must be omitted when not provided (not sent as null)
        assert "alias" not in captured["json"]

    def test_add_ssh_source_with_alias(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["json"] = json.loads(request.content)
            return httpx.Response(
                201,
                json={
                    "transport": "ssh",
                    "location": "hostA:/path/to/docs",
                    "alias": "hostA-docs",
                    "slug": "hostA-docs",
                },
            )

        with _patch_client_transport(handler):
            result = runner.invoke(
                app,
                [
                    "add",
                    "--context",
                    "ctx1",
                    "--source",
                    "ssh://hostA/path/to/docs",
                    "--alias",
                    "hostA-docs",
                ],
            )

        assert result.exit_code == 0, result.output
        assert captured["json"]["transport"] == "ssh"
        assert captured["json"]["location"] == "hostA:/path/to/docs"
        assert captured["json"]["alias"] == "hostA-docs"

    def test_add_wireguard_source(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["json"] = json.loads(request.content)
            return httpx.Response(
                201,
                json={
                    "transport": "wireguard",
                    "location": "hostB:/path/to/docs",
                    "slug": "docs",
                },
            )

        with _patch_client_transport(handler):
            result = runner.invoke(
                app,
                ["add", "--context", "ctx2", "--source", "wg://hostB/path/to/docs"],
            )

        assert result.exit_code == 0, result.output
        assert captured["json"]["transport"] == "wireguard"
        assert captured["json"]["location"] == "hostB:/path/to/docs"

    def test_add_source_with_renderer(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["json"] = json.loads(request.content)
            return httpx.Response(
                201,
                json={
                    "transport": "local",
                    "location": "/path/to/project-docs",
                    "slug": "project-docs",
                    "renderer": {"md_engine": "python-markdown"},
                },
            )

        with _patch_client_transport(handler):
            result = runner.invoke(
                app,
                [
                    "add",
                    "--context",
                    "ctx1",
                    "--source",
                    "/path/to/project-docs",
                    "--md-engine",
                    "python-markdown",
                ],
            )

        assert result.exit_code == 0, result.output
        assert captured["json"]["renderer"] == {"md_engine": "python-markdown"}


# --------------------------------------------------------------------------- list
class TestListCommand:
    def test_list_contexts(self) -> None:
        def handler(request: httpx.Request) -> httpx.Response:
            assert request.method == "GET"
            return httpx.Response(
                200,
                json={
                    "contexts": [
                        {
                            "id": "ctx1",
                            "sources": [
                                {
                                    "transport": "local",
                                    "location": "/path/to/project-docs",
                                    "alias": "project-docs",
                                    "slug": "project-docs",
                                }
                            ],
                            "renderer": {"md_engine": "pandoc", "hl_engine": "pygments"},
                        },
                        {
                            "id": "ctx2",
                            "sources": [],
                        },
                    ]
                },
            )

        with _patch_client_transport(handler):
            result = runner.invoke(app, ["list"])

        assert result.exit_code == 0, result.output
        assert "ctx1" in result.output
        assert "ctx2" in result.output
        assert "project-docs" in result.output

    def test_list_empty(self) -> None:
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(200, json={"contexts": []})

        with _patch_client_transport(handler):
            result = runner.invoke(app, ["list"])

        assert result.exit_code == 0, result.output
        assert "No contexts" in result.output


# --------------------------------------------------------------------------- remove
class TestRemoveCommand:
    def test_remove_source(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["method"] = request.method
            captured["url"] = str(request.url)
            return httpx.Response(204)

        with _patch_client_transport(handler):
            result = runner.invoke(
                app, ["remove", "--context", "ctx1", "--slug", "project-docs"]
            )

        assert result.exit_code == 0, result.output
        assert captured["method"] == "DELETE"
        assert "/contexts/ctx1/sources/project-docs" in captured["url"]


# --------------------------------------------------------------------------- delete
class TestDeleteCommand:
    def test_delete_context(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["method"] = request.method
            captured["url"] = str(request.url)
            return httpx.Response(204)

        with _patch_client_transport(handler):
            result = runner.invoke(app, ["delete", "--context", "ctx1"])

        assert result.exit_code == 0, result.output
        assert captured["method"] == "DELETE"
        assert "/contexts/ctx1" in captured["url"]


# --------------------------------------------------------------------------- reload
class TestReloadCommand:
    def test_reload_context(self) -> None:
        captured: dict = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["method"] = request.method
            captured["url"] = str(request.url)
            return httpx.Response(200, json={"id": "ctx1", "sources": []})

        with _patch_client_transport(handler):
            result = runner.invoke(app, ["reload", "--context", "ctx1"])

        assert result.exit_code == 0, result.output
        assert captured["method"] == "POST"
        assert "/contexts/ctx1/reload" in captured["url"]


# --------------------------------------------------------------------------- error handling
class TestErrorHandling:
    def test_http_error_exits_nonzero(self) -> None:
        def handler(request: httpx.Request) -> httpx.Response:
            return httpx.Response(
                409,
                json={"error": "Conflict", "resource": "source", "id": "ctx1"},
            )

        with _patch_client_transport(handler):
            result = runner.invoke(app, ["create", "--context", "ctx1"])

        assert result.exit_code == 1
