import json

import pytest
from django.core import files
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import QueryDict
from django.test import RequestFactory
from django.test.client import MULTIPART_CONTENT
from pydantic import BaseModel

from hatchway import ApiError, Body, QueryOrBody, api_view
from hatchway.view import ApiView


def test_basic_view():
    """
    Tests that a view with simple types works correctly
    """

    @api_view
    def test_view(
        request,
        a: int,
        b: QueryOrBody[int | None] = None,
        c: str = "x",
    ) -> str:
        if b is None:
            return c * a
        else:
            return c * (a - b)

    # Call it with a few different patterns to verify it's type coercing right
    factory = RequestFactory()

    # Implicit query param
    response = test_view(factory.get("/test/?a=4"))
    assert json.loads(response.content) == "xxxx"

    # QueryOrBody pulling from query
    response = test_view(factory.get("/test/?a=4&b=2"))
    assert json.loads(response.content) == "xx"

    # QueryOrBody pulling from formdata body
    response = test_view(factory.post("/test/?a=4", {"b": "3"}))
    assert json.loads(response.content) == "x"

    # QueryOrBody pulling from JSON body
    response = test_view(
        factory.post(
            "/test/?a=4", json.dumps({"b": 3}), content_type="application/json"
        )
    )
    assert json.loads(response.content) == "x"

    # Implicit Query not pulling from body
    response = test_view(factory.post("/test/", {"a": 4, "b": 3}))
    assert response.status_code == 400


def test_body_direct():
    """
    Tests that a Pydantic model with BodyDirect gets its fields from the top level
    """

    class TestModel(BaseModel):
        number: int
        name: str

    @api_view
    def test_view(request, data: TestModel) -> int:
        return data.number

    factory = RequestFactory()

    # formdata version
    response = test_view(factory.post("/test/", {"number": "123", "name": "Andrew"}))
    assert json.loads(response.content) == 123

    # JSON body version
    response = test_view(
        factory.post(
            "/test/",
            json.dumps({"number": "123", "name": "Andrew"}),
            content_type="application/json",
        )
    )
    assert json.loads(response.content) == 123


@pytest.mark.parametrize("arg_type", [list, list[int], set[int]])
def test_list_arg(arg_type):
    """
    Tests that a collection argument does not pull from the path
    """

    @api_view
    def test_view(request, data: arg_type) -> int:
        return list(data)[0]

    factory = RequestFactory()

    response = test_view(factory.get("/test/", {"data": "2"}), data=1)
    assert json.loads(response.content) == 2


def test_list_response():
    """
    Tests that a view with a list response type works correctly with both
    dicts and pydantic model instances.
    """

    class TestModel(BaseModel):
        number: int
        name: str

    @api_view
    def test_view_dict(request) -> list[TestModel]:
        return [
            {"name": "Andrew", "number": 1},  # type:ignore
            {"name": "Alice", "number": 0},  # type:ignore
        ]

    @api_view
    def test_view_model(request) -> list[TestModel]:
        return [TestModel(name="Andrew", number=1), TestModel(name="Alice", number=0)]

    response = test_view_dict(RequestFactory().get("/test/"))
    assert json.loads(response.content) == [
        {"name": "Andrew", "number": 1},
        {"name": "Alice", "number": 0},
    ]

    response = test_view_model(RequestFactory().get("/test/"))
    assert json.loads(response.content) == [
        {"name": "Andrew", "number": 1},
        {"name": "Alice", "number": 0},
    ]


def test_patch_body():
    """
    Tests that PATCH also gets its body parsed
    """

    @api_view.patch
    def test_view(request, a: Body[int]):
        return a

    factory = RequestFactory()
    response = test_view(
        factory.patch(
            "/test/",
            content_type=MULTIPART_CONTENT,
            data=factory._encode_data({"a": "42"}, MULTIPART_CONTENT),
        )
    )
    assert json.loads(response.content) == 42


def test_file_body():
    """
    Tests that file uploads work right
    """

    @api_view.post
    def test_view(request, a: Body[int], b: files.File) -> str:
        return str(a) + b.read().decode("ascii")

    factory = RequestFactory()
    uploaded_file = SimpleUploadedFile(
        "file.txt",
        b"MY FILE IS AMAZING",
        content_type="text/plain",
    )
    response = test_view(
        factory.post(
            "/test/",
            data={"a": 42, "b": uploaded_file},
        )
    )
    assert json.loads(response.content) == "42MY FILE IS AMAZING"


def test_no_response():
    """
    Tests that a view with no response type returns the contents verbatim
    """

    @api_view
    def test_view(request):
        return [1, "woooooo"]

    response = test_view(RequestFactory().get("/test/"))
    assert json.loads(response.content) == [1, "woooooo"]


def test_wrong_method():
    """
    Tests that a view with a method limiter works
    """

    @api_view.get
    def test_view(request):
        return "yay"

    response = test_view(RequestFactory().get("/test/"))
    assert json.loads(response.content) == "yay"

    response = test_view(RequestFactory().post("/test/"))
    assert response.status_code == 405


def test_api_error():
    """
    Tests that ApiError propagates right
    """

    @api_view.get
    def test_view(request):
        raise ApiError(401, "you did a bad thing")

    response = test_view(RequestFactory().get("/test/"))
    assert json.loads(response.content) == {"error": "you did a bad thing"}
    assert response.status_code == 401


def test_unusable_type():
    """
    Tests that you get a nice error when you use a type on an input that
    Pydantic doesn't understand.
    """

    with pytest.raises(ValueError):

        @api_view.get
        def test_view(request, a: RequestFactory):
            pass


def test_not_provided():
    """
    Tests that missing a parameter gives a nice error
    """

    @api_view.get
    def test_view(
        request,
        id: str,
    ):
        return 1

    response = test_view(RequestFactory().get("/test/"))
    assert json.loads(response.content) == {"error": "invalid_input"}
    assert response.status_code == 400


def test_string_not_list():
    """
    Tests that providing a string not a list for a variable is handled well
    """

    @api_view.get
    def test_view(
        request,
        id: list[str] | None,
    ):
        return 1

    response = test_view(RequestFactory().get("/test/?id=notalist"))
    assert json.loads(response.content) == {
        "error": "invalid_input",
        "error_details": [
            {
                "loc": ["id"],
                "msg": "value is not a valid list",
                "type": "type_error.list",
            }
        ],
    }
    assert response.status_code == 400


def test_get_values():
    """
    Tests that ApiView.get_values correctly handles lists
    """

    assert ApiView.get_values({"a": 2, "b": [3, 4]}) == {"a": 2, "b": [3, 4]}
    assert ApiView.get_values({"a": 2, "b[]": [3, 4]}) == {"a": 2, "b": [3, 4]}
    assert ApiView.get_values(QueryDict("a=2&b=3&b=4")) == {"a": "2", "b": ["3", "4"]}
    assert ApiView.get_values(QueryDict("a=2&b[]=3&b[]=4")) == {
        "a": "2",
        "b": ["3", "4"],
    }
    assert ApiView.get_values(QueryDict("a=2&b=3")) == {"a": "2", "b": "3"}
    assert ApiView.get_values(QueryDict("a=2&b[]=3")) == {"a": "2", "b": ["3"]}
    assert ApiView.get_values(QueryDict("a[b]=1")) == {"a": {"b": "1"}}
    assert ApiView.get_values(QueryDict("a[b]=1&a[c]=2")) == {"a": {"b": "1", "c": "2"}}
    assert ApiView.get_values(QueryDict("a[b][c]=1")) == {"a": {"b": {"c": "1"}}}
    assert ApiView.get_values(QueryDict("a[b][c][]=1")) == {"a": {"b": {"c": ["1"]}}}
    assert ApiView.get_values(QueryDict("a[b][]=1&a[b][]=2")) == {
        "a": {"b": ["1", "2"]}
    }
