import json
from typing import Callable, Optional

import httpx
import pytest
from respx import Router

from tests.integration.conftest import Route
from wriftai import Client, ClientOptions
from wriftai.common_types import ModelVisibility, SortDirection
from wriftai.models import (
    CreateModelParams,
    ModelPaginationOptions,
    ModelsSortBy,
    UpdateModelParams,
)
from wriftai.pagination import PaginatedResponse, PaginationOptions


@pytest.mark.asyncio
@pytest.mark.parametrize("async_flag", [True, False])
@pytest.mark.parametrize("owner", [None, "test_username"])
async def test_list(
    mock_router: Callable[..., Router], async_flag: bool, owner: Optional[str]
) -> None:
    expected_owner = owner or "test_username"
    path = f"/models/{expected_owner}" if owner else "/models"
    expected_cursor = "abc123"
    expected_page_size = "10"
    test_category_slugs = ["slug-1", "slug-2", "slug-3"]

    expected_json = {
        "items": [
            {
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "name": "test_model_name",
                "created_at": "2025-08-01T10:00:00Z",
                "visibility": ModelVisibility.public,
                "description": "A computer vision model.",
                "updated_at": "2025-08-15T14:30:00Z",
                "owner": {
                    "id": "user-001",
                    "username": expected_owner,
                    "avatar_url": "https://example.com/avatar.png",
                },
                "hardware": {
                    "name": "test hardware name",
                    "identifier": "test hardware identifier",
                },
                "predictions_count": 2048,
                "categories": [
                    {
                        "name": "model category name",
                        "slug": "model category slug",
                    },
                ],
            }
        ],
        "next_cursor": "abc123",
        "previous_cursor": None,
        "next_url": "/models?cursor=abc123",
        "previous_url": None,
    }

    call_count = 0

    def request_assertions(request: httpx.Request) -> None:
        nonlocal call_count
        call_count += 1
        assert request.url.params.get("cursor") == expected_cursor
        assert request.url.params.get("page_size") == expected_page_size
        assert request.url.params.get("sort_by") == ModelsSortBy.CREATED_AT
        assert request.url.params.get("sort_direction") == SortDirection.ASC
        assert request.url.params.get_list("category_slugs") == test_category_slugs
        assert set(request.url.params.keys()).issubset({
            "cursor",
            "page_size",
            "sort_by",
            "sort_direction",
            "category_slugs",
        })

    router = mock_router(
        route=Route(
            method="GET",
            path=path,
            status_code=200,
            json=expected_json,
        ),
        callback=request_assertions,
    )

    client = Client(
        client_options=ClientOptions(
            headers={"Authorization": "test-token"},
            timeout=httpx.Timeout(15),
            transport=httpx.MockTransport(router.handler),
        )
    )

    pagination_options = ModelPaginationOptions({
        "cursor": expected_cursor,
        "page_size": int(expected_page_size),
        "sort_by": ModelsSortBy.CREATED_AT,
        "sort_direction": SortDirection.ASC,
        "category_slugs": test_category_slugs,
    })

    if async_flag:
        response = await client.models.async_list(
            owner=owner, pagination_options=pagination_options
        )
    else:
        response = client.models.list(
            owner=owner, pagination_options=pagination_options
        )

    # expected_json is a dictionary.
    # Although the structure matches the expected fields, static type checkers like mypy
    # may not be able to verify this due to dynamic typing.
    assert response == PaginatedResponse(**expected_json)  # type:ignore[arg-type]
    assert call_count == 1


@pytest.mark.asyncio
@pytest.mark.parametrize("async_flag", [True, False])
async def test_delete(mock_router: Callable[..., Router], async_flag: bool) -> None:
    test_owner = "test_owner"
    test_model_name = "dummy_model"

    call_count = 0

    def request_assertions(request: httpx.Request) -> None:
        nonlocal call_count
        call_count += 1

    router = mock_router(
        route=Route(
            method="DELETE",
            path=f"/models/{test_owner}/{test_model_name}",
            status_code=204,
            json={},
        ),
        callback=request_assertions,
    )

    client = Client(
        client_options=ClientOptions(
            headers={"Authorization": "test-token"},
            timeout=httpx.Timeout(15),
            transport=httpx.MockTransport(router.handler),
        )
    )

    if async_flag:
        await client.models.async_delete(identifier=f"{test_owner}/{test_model_name}")
    else:
        client.models.delete(identifier=f"{test_owner}/{test_model_name}")

    assert call_count == 1


@pytest.mark.asyncio
@pytest.mark.parametrize("async_flag", [True, False])
async def test_get(mock_router: Callable[..., Router], async_flag: bool) -> None:
    test_owner = "test_owner"
    test_model_name = "dummy_model"

    expected_json = {
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "name": test_model_name,
        "created_at": "2025-08-01T10:00:00Z",
        "visibility": ModelVisibility.public,
        "description": "A computer vision model.",
        "updated_at": "2025-08-15T14:30:00Z",
        "source_url": "https://example.com/source",
        "license_url": "https://example.com/license",
        "paper_url": "https://example.com/paper",
        "owner": {
            "id": "user-001",
            "username": test_owner,
            "avatar_url": "https://example.com/avatar.png",
        },
        "latest_version": {
            "number": 1,
            "release_notes": "Initial release.",
            "created_at": "2025-08-10T09:00:00Z",
            "schemas": {
                "prediction": {
                    "input": {"key1": "value1", "key2": 123},
                    "output": {"result": True, "message": "Success"},
                }
            },
            "container_image_digest": "sha256:abc123def456ghi",
        },
        "hardware": {
            "name": "test hardware name",
            "identifier": "test hardware identifier",
        },
        "predictions_count": 2048,
        "overview": "overview of the model",
        "categories": [
            {
                "name": "model category name",
                "slug": "model category slug",
            },
        ],
    }

    call_count = 0

    def request_assertions(request: httpx.Request) -> None:
        nonlocal call_count
        call_count += 1

    router = mock_router(
        route=Route(
            method="GET",
            path=f"/models/{test_owner}/{test_model_name}",
            status_code=200,
            json=expected_json,
        ),
        callback=request_assertions,
    )

    client = Client(
        client_options=ClientOptions(
            headers={"Authorization": "test-token"},
            timeout=httpx.Timeout(15),
            transport=httpx.MockTransport(router.handler),
        )
    )

    if async_flag:
        response = await client.models.async_get(
            identifier=f"{test_owner}/{test_model_name}"
        )
    else:
        response = client.models.get(identifier=f"{test_owner}/{test_model_name}")

    assert response == expected_json
    assert call_count == 1


@pytest.mark.asyncio
@pytest.mark.parametrize("async_flag", [True, False])
async def test_create(mock_router: Callable[..., Router], async_flag: bool) -> None:
    params: CreateModelParams = {
        "name": "test_model",
        "visibility": ModelVisibility.private,
        "hardware_identifier": "test-identifier",
        "description": "Test model description",
        "source_url": "https://example.com/source",
        "license_url": "https://example.com/license",
        "paper_url": "https://example.com/paper",
        "overview": "overview of the model",
        "category_slugs": ["slug-1", "slug-2", "slug-3"],
    }

    expected_json = {
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "name": params["name"],
        "created_at": "2025-08-01T10:00:00Z",
        "visibility": params["visibility"],
        "description": params["description"],
        "updated_at": "2025-08-15T14:30:00Z",
        "source_url": params["source_url"],
        "license_url": params["license_url"],
        "paper_url": params["paper_url"],
        "owner": {
            "id": "user-001",
            "username": "Kunal_Kumar",
            "avatar_url": "https://example.com/avatar.png",
        },
        "latest_version": {
            "number": 1,
            "release_notes": "Initial release.",
            "created_at": "2025-08-10T09:00:00Z",
            "schemas": {
                "prediction": {
                    "input": {"key1": "value1", "key2": 123},
                    "output": {"result": True, "message": "Success"},
                }
            },
            "container_image_digest": "sha256:abc123def456ghi",
        },
        "hardware": {
            "name": "test hardware name",
            "identifier": params["hardware_identifier"],
        },
        "predictions_count": 2048,
        "overview": "overview of the model",
        "categories": [
            {
                "name": "model category name",
                "slug": "model category slug",
            },
        ],
    }

    call_count = 0

    def request_assertions(request: httpx.Request) -> None:
        nonlocal call_count
        call_count += 1
        json_payload = json.loads(request.content.decode())
        assert json_payload == params

    router = mock_router(
        route=Route(
            method="POST",
            path="/models",
            status_code=200,
            json=expected_json,
        ),
        callback=request_assertions,
    )

    client = Client(
        client_options=ClientOptions(
            headers={"Authorization": "test-token"},
            timeout=httpx.Timeout(15),
            transport=httpx.MockTransport(router.handler),
        )
    )

    if async_flag:
        response = await client.models.async_create(params)
    else:
        response = client.models.create(params)
    assert response == expected_json
    assert call_count == 1


@pytest.mark.asyncio
@pytest.mark.parametrize("async_flag", [True, False])
async def test_update(mock_router: Callable[..., Router], async_flag: bool) -> None:
    test_owner = "test_owner"
    test_model_name = "dummy_model"
    description = "Updated description"
    source_url = "https://example.com/updated_source"
    license_url = "https://license.com/updated_license"
    paper_url = "https://paper.com/updated_paper"
    hardware_identifier = "Updated Hardware identifier"
    overview = "overview of the model"

    expected_json = {
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "name": test_model_name,
        "created_at": "2025-08-01T10:00:00Z",
        "visibility": ModelVisibility.public,
        "description": description,
        "updated_at": "2025-08-15T14:30:00Z",
        "source_url": source_url,
        "license_url": license_url,
        "paper_url": paper_url,
        "owner": {
            "id": "user-001",
            "username": test_owner,
            "avatar_url": "https://example.com/avatar.png",
        },
        "latest_version": {
            "number": 1,
            "release_notes": "Initial release.",
            "created_at": "2025-08-10T09:00:00Z",
            "schemas": {
                "prediction": {
                    "input": {"key1": "value1", "key2": 123},
                    "output": {"result": True, "message": "Success"},
                }
            },
            "container_image_digest": "sha256:abc123def456ghi",
        },
        "hardware": {
            "name": "test hardware name",
            "identifier": hardware_identifier,
        },
        "predictions_count": 2048,
        "overview": "overview of the model",
        "categories": [
            {
                "name": "model category name",
                "slug": "model category slug",
            },
        ],
    }
    payload: UpdateModelParams = {
        "description": description,
        "source_url": source_url,
        "license_url": license_url,
        "paper_url": paper_url,
        "hardware_identifier": hardware_identifier,
        "overview": overview,
        "category_slugs": ["slug-1", "slug-2", "slug-3"],
    }

    call_count = 0

    def request_assertions(request: httpx.Request) -> None:
        nonlocal call_count
        call_count += 1
        json_payload = json.loads(request.content.decode())
        assert json_payload == payload

    router = mock_router(
        route=Route(
            method="PATCH",
            path=f"/models/{test_owner}/{test_model_name}",
            status_code=200,
            json=expected_json,
        ),
        callback=request_assertions,
    )
    client = Client(
        client_options=ClientOptions(
            headers={"Authorization": "test-token"},
            timeout=httpx.Timeout(15),
            transport=httpx.MockTransport(router.handler),
        )
    )
    if async_flag:
        response = await client.models.async_update(
            identifier=f"{test_owner}/{test_model_name}",
            params=payload,
        )
    else:
        response = client.models.update(
            identifier=f"{test_owner}/{test_model_name}",
            params=payload,
        )
    assert response == expected_json
    assert call_count == 1


@pytest.mark.asyncio
@pytest.mark.parametrize("async_flag", [True, False])
async def test_search(mock_router: Callable[..., Router], async_flag: bool) -> None:
    expected_cursor = "abc123"
    expected_page_size = "10"
    expected_query = "dummy_query"
    expected_json = {
        "items": [
            {
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "name": "test_model_name",
                "created_at": "2025-08-01T10:00:00Z",
                "visibility": ModelVisibility.public,
                "description": "A computer vision model.",
                "updated_at": "2025-08-15T14:30:00Z",
                "owner": {
                    "id": "user-001",
                    "username": "test_username",
                    "avatar_url": "https://example.com/avatar.png",
                },
                "hardware": {
                    "name": "test hardware name",
                    "identifier": "test-identifier",
                },
                "predictions_count": 2048,
                "categories": [
                    {
                        "name": "model category name",
                        "slug": "model category slug",
                    },
                ],
            }
        ],
        "next_cursor": "abc123",
        "previous_cursor": None,
        "next_url": "/search/models?cursor=abc123",
        "previous_url": None,
    }

    call_count = 0

    def request_assertions(request: httpx.Request) -> None:
        nonlocal call_count
        call_count += 1
        assert request.url.params.get("cursor") == expected_cursor
        assert request.url.params.get("page_size") == expected_page_size
        assert request.url.params.get("q") == expected_query
        assert set(request.url.params.keys()).issubset({
            "cursor",
            "page_size",
            "q",
        })

    router = mock_router(
        route=Route(
            method="GET",
            path="/search/models",
            status_code=200,
            json=expected_json,
        ),
        callback=request_assertions,
    )
    client = Client(
        client_options=ClientOptions(
            headers={"Authorization": "test-token"},
            timeout=httpx.Timeout(15),
            transport=httpx.MockTransport(router.handler),
        )
    )

    pagiantion_options = PaginationOptions({
        "cursor": expected_cursor,
        "page_size": int(expected_page_size),
    })

    if async_flag:
        response = await client.models.async_search(
            q=expected_query, pagination_options=pagiantion_options
        )
    else:
        response = client.models.search(
            q=expected_query, pagination_options=pagiantion_options
        )

    # expected_json is a dictionary.
    # Although the structure matches the expected fields, static type checkers like mypy
    # may not be able to verify this due to dynamic typing.
    assert response == PaginatedResponse(**expected_json)  # type:ignore[arg-type]
    assert call_count == 1
