import json
from typing import Any
from unittest.mock import patch

import httpx
import pytest

from linwheel import LinWheel, LinWheelApiError, LinWheelAuthError


TEST_API_KEY = "lw_sk_test1234567890abcdef"
TEST_BASE_URL = "https://test.linwheel.io"


def capture_transport(
    status_code: int = 200,
    body: dict[str, Any] | None = None,
) -> tuple[httpx.MockTransport, list[httpx.Request]]:
    """Returns a mock transport and a list that captures requests."""
    response_body = body or {}
    captured: list[httpx.Request] = []

    def handler(request: httpx.Request) -> httpx.Response:
        captured.append(request)
        return httpx.Response(status_code=status_code, json=response_body)

    return httpx.MockTransport(handler), captured


def make_client(
    transport: httpx.MockTransport,
    signing_secret: str | None = None,
) -> LinWheel:
    lw = LinWheel(
        api_key=TEST_API_KEY,
        base_url=TEST_BASE_URL,
        signing_secret=signing_secret,
    )
    lw._client = httpx.Client(transport=transport)
    return lw


# ── Auth ──────────────────────────────────────────────────────


class TestAuth:
    def test_sends_bearer_token(self):
        transport, captured = capture_transport(body={"ok": True})
        lw = make_client(transport)
        lw.analyze(text="test")

        assert captured[0].headers["authorization"] == f"Bearer {TEST_API_KEY}"

    def test_sends_hmac_signature(self):
        transport, captured = capture_transport(body={"ok": True})
        lw = make_client(transport, signing_secret="secret123")
        lw.analyze(text="test")

        sig = captured[0].headers["x-lw-signature"]
        assert sig.startswith("t=")
        assert ",v1=" in sig

    def test_no_signature_without_secret(self):
        transport, captured = capture_transport(body={"ok": True})
        lw = make_client(transport)
        lw.analyze(text="test")

        assert "x-lw-signature" not in captured[0].headers


# ── Error handling ────────────────────────────────────────────


class TestErrors:
    def test_raises_auth_error_on_401(self):
        transport, _ = capture_transport(
            status_code=401, body={"error": "Invalid or missing API key"}
        )
        lw = make_client(transport)

        with pytest.raises(LinWheelAuthError):
            lw.analyze(text="test")

    def test_raises_api_error_on_422(self):
        transport, _ = capture_transport(
            status_code=422, body={"error": "Validation failed"}
        )
        lw = make_client(transport)

        with pytest.raises(LinWheelApiError):
            lw.analyze(text="test")

    def test_raises_api_error_on_500(self):
        transport, _ = capture_transport(
            status_code=500, body={"error": "Internal server error"}
        )
        lw = make_client(transport)

        with pytest.raises(LinWheelApiError) as exc_info:
            lw.analyze(text="test")
        assert exc_info.value.status_code == 500

    def test_error_includes_response_body(self):
        body = {"error": "Bad request", "details": {"text": "required"}}
        transport, _ = capture_transport(status_code=400, body=body)
        lw = make_client(transport)

        with pytest.raises(LinWheelApiError) as exc_info:
            lw.analyze(text="")
        assert exc_info.value.response_body == body


# ── Content processing ────────────────────────────────────────


class TestAnalyze:
    def test_posts_to_analyze(self):
        transport, captured = capture_transport(body={"score": 8})
        lw = make_client(transport)
        result = lw.analyze(text="my content", context="buildlog")

        req = captured[0]
        assert req.method == "POST"
        assert "/api/agent/analyze" in str(req.url)
        body = json.loads(req.content)
        assert body == {"text": "my content", "context": "buildlog"}
        assert result == {"score": 8}


class TestReshape:
    def test_posts_to_reshape(self):
        transport, captured = capture_transport(body={"posts": []})
        lw = make_client(transport)
        lw.reshape(
            text="source content",
            angles=["field_note", "contrarian"],
            save_drafts=True,
        )

        body = json.loads(captured[0].content)
        assert body == {
            "text": "source content",
            "angles": ["field_note", "contrarian"],
            "saveDrafts": True,
        }


class TestRefine:
    def test_posts_to_refine(self):
        transport, captured = capture_transport(body={"refined": "..."})
        lw = make_client(transport)
        lw.refine(text="draft", intensity="heavy")

        body = json.loads(captured[0].content)
        assert body["intensity"] == "heavy"
        assert "/api/agent/refine" in str(captured[0].url)


class TestSplit:
    def test_posts_to_split(self):
        transport, captured = capture_transport(body={"series": []})
        lw = make_client(transport)
        lw.split(text="long content", max_posts=4)

        body = json.loads(captured[0].content)
        assert body["maxPosts"] == 4
        assert "/api/agent/split" in str(captured[0].url)


# ── Drafting ──────────────────────────────────────────────────


class TestDraft:
    def test_posts_to_draft(self):
        transport, captured = capture_transport(body={"id": "post_1"})
        lw = make_client(transport)
        lw.draft(full_text="post content", post_type="field_note")

        body = json.loads(captured[0].content)
        assert body["fullText"] == "post content"
        assert "/api/agent/draft" in str(captured[0].url)


class TestBundle:
    def test_posts_to_bundle(self):
        transport, captured = capture_transport(body={"id": "post_2"})
        lw = make_client(transport)
        lw.bundle(
            full_text="bundled post",
            image_headline_text="Big Title",
            image_style_preset="dark_mode",
            carousel_slides=[{"headlineText": "Slide 1"}],
        )

        body = json.loads(captured[0].content)
        assert body["imageHeadlineText"] == "Big Title"
        assert len(body["carouselSlides"]) == 1
        assert "/api/agent/bundle" in str(captured[0].url)


# ── Posts resource ────────────────────────────────────────────


class TestPostsList:
    def test_gets_posts_with_query_params(self):
        transport, captured = capture_transport(body={"posts": []})
        lw = make_client(transport)
        lw.posts.list(approved=False, limit=10)

        req = captured[0]
        assert req.method == "GET"
        url_str = str(req.url)
        assert "approved=false" in url_str
        assert "limit=10" in url_str


class TestPostsGet:
    def test_gets_single_post(self):
        transport, captured = capture_transport(body={"id": "post_1"})
        lw = make_client(transport)
        lw.posts.get("post_1")

        assert "/api/agent/posts/post_1" in str(captured[0].url)
        assert captured[0].method == "GET"


class TestPostsUpdate:
    def test_patches_post(self):
        transport, captured = capture_transport(body={"id": "post_1"})
        lw = make_client(transport)
        lw.posts.update("post_1", full_text="updated")

        assert captured[0].method == "PATCH"
        body = json.loads(captured[0].content)
        assert body["fullText"] == "updated"


class TestPostsApprove:
    def test_approves_post(self):
        transport, captured = capture_transport(body={"approved": True})
        lw = make_client(transport)
        lw.posts.approve("post_1", approved=True)

        assert "/api/agent/posts/post_1/approve" in str(captured[0].url)
        body = json.loads(captured[0].content)
        assert body == {"approved": True}


class TestPostsSchedule:
    def test_schedules_post(self):
        transport, captured = capture_transport(
            body={"scheduledAt": "2026-02-24T09:00:00Z"}
        )
        lw = make_client(transport)
        lw.posts.schedule(
            "post_1",
            scheduled_at="2026-02-24T09:00:00Z",
            auto_publish=True,
        )

        assert captured[0].method == "PATCH"
        assert "/schedule" in str(captured[0].url)


class TestPostsImage:
    def test_creates_image(self):
        transport, captured = capture_transport(body={"imageUrl": "https://..."})
        lw = make_client(transport)
        lw.posts.image("post_1", headline_text="Big Title", style_preset="dark_mode")

        assert "/api/agent/posts/post_1/image" in str(captured[0].url)
        body = json.loads(captured[0].content)
        assert body["stylePreset"] == "dark_mode"


class TestPostsCarousel:
    def test_creates_carousel(self):
        transport, captured = capture_transport(body={"pdfUrl": "https://..."})
        lw = make_client(transport)
        lw.posts.carousel(
            "post_1",
            slides=[
                {"headlineText": "Title"},
                {"headlineText": "Content", "caption": "Details"},
            ],
            style_preset="accent_bar",
        )

        assert "/api/agent/posts/post_1/carousel" in str(captured[0].url)
        body = json.loads(captured[0].content)
        assert len(body["slides"]) == 2
        assert body["stylePreset"] == "accent_bar"


# ── Voice profiles resource ────────────────────────────────────


class TestVoiceProfilesList:
    def test_gets_voice_profiles(self):
        response_body = {
            "profiles": [
                {"id": "vp_1", "name": "My Voice", "samples": ["sample"], "isActive": True}
            ],
            "activeProfileId": "vp_1",
        }
        transport, captured = capture_transport(body=response_body)
        lw = make_client(transport)
        result = lw.voice_profiles.list()

        assert captured[0].method == "GET"
        assert "/api/agent/voice-profiles" in str(captured[0].url)
        assert len(result["profiles"]) == 1
        assert result["activeProfileId"] == "vp_1"

    def test_returns_empty_list(self):
        transport, captured = capture_transport(
            body={"profiles": [], "activeProfileId": None}
        )
        lw = make_client(transport)
        result = lw.voice_profiles.list()
        assert result["profiles"] == []
        assert result["activeProfileId"] is None


class TestVoiceProfilesCreate:
    def test_creates_profile_with_samples(self):
        profile = {
            "id": "vp_new",
            "name": "Dev Voice",
            "description": "Technical, direct",
            "samples": ["Sample 1", "Sample 2", "Sample 3"],
            "isActive": True,
        }
        transport, captured = capture_transport(body={"profile": profile})
        lw = make_client(transport)
        result = lw.voice_profiles.create(
            name="Dev Voice",
            description="Technical, direct",
            samples=["Sample 1", "Sample 2", "Sample 3"],
            is_active=True,
        )

        assert captured[0].method == "POST"
        assert "/api/agent/voice-profiles" in str(captured[0].url)
        body = json.loads(captured[0].content)
        assert body["name"] == "Dev Voice"
        assert body["description"] == "Technical, direct"
        assert len(body["samples"]) == 3
        assert body["isActive"] is True
        assert result["profile"]["id"] == "vp_new"

    def test_creates_profile_without_optional_fields(self):
        transport, captured = capture_transport(
            body={"profile": {"id": "vp_2", "name": "Minimal", "samples": ["one"]}}
        )
        lw = make_client(transport)
        lw.voice_profiles.create(name="Minimal", samples=["one"])

        body = json.loads(captured[0].content)
        assert body["name"] == "Minimal"
        assert body["samples"] == ["one"]
        assert "description" not in body
        assert "isActive" not in body

    def test_sends_auth_header_on_create(self):
        transport, captured = capture_transport(
            body={"profile": {"id": "vp_3"}}
        )
        lw = make_client(transport)
        lw.voice_profiles.create(name="Test", samples=["s1"])

        assert captured[0].headers["authorization"] == f"Bearer {TEST_API_KEY}"


class TestVoiceProfilesDelete:
    def test_deletes_profile(self):
        transport, captured = capture_transport(body={"deleted": True, "id": "vp_1"})
        lw = make_client(transport)
        result = lw.voice_profiles.delete("vp_1")

        assert captured[0].method == "DELETE"
        assert "/api/agent/voice-profiles/vp_1" in str(captured[0].url)
        assert result["deleted"] is True

    def test_delete_sends_correct_path(self):
        transport, captured = capture_transport(body={"deleted": True, "id": "abc-123"})
        lw = make_client(transport)
        lw.voice_profiles.delete("abc-123")

        assert "/api/agent/voice-profiles/abc-123" in str(captured[0].url)


class TestVoiceProfilesActivate:
    def test_activates_profile(self):
        transport, captured = capture_transport(body={"activated": True, "id": "vp_1"})
        lw = make_client(transport)
        result = lw.voice_profiles.activate("vp_1")

        assert captured[0].method == "POST"
        assert "/api/agent/voice-profiles/vp_1/activate" in str(captured[0].url)
        assert result["activated"] is True

    def test_activate_sends_auth_header(self):
        transport, captured = capture_transport(body={"activated": True, "id": "vp_1"})
        lw = make_client(transport)
        lw.voice_profiles.activate("vp_1")

        assert captured[0].headers["authorization"] == f"Bearer {TEST_API_KEY}"

    def test_activate_401_raises_auth_error(self):
        transport, _ = capture_transport(
            status_code=401, body={"error": "Invalid or missing API key"}
        )
        lw = make_client(transport)
        with pytest.raises(LinWheelAuthError):
            lw.voice_profiles.activate("vp_1")


# ── Config ────────────────────────────────────────────────────


class TestConfig:
    def test_strips_trailing_slash(self):
        transport, captured = capture_transport(body={})
        lw = LinWheel(
            api_key=TEST_API_KEY,
            base_url="https://test.linwheel.io/",
        )
        lw._client = httpx.Client(transport=transport)
        lw.analyze(text="test")

        assert "test.linwheel.io/api/agent/analyze" in str(captured[0].url)
        assert "test.linwheel.io//api" not in str(captured[0].url)

    def test_context_manager(self):
        transport, _ = capture_transport(body={})
        with LinWheel(api_key=TEST_API_KEY, base_url=TEST_BASE_URL) as lw:
            lw._client = httpx.Client(transport=transport)
            lw.analyze(text="test")
