from functools import partial

from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from django_downloadview import DownloadResponse
from pydantic import BaseModel

from ichec_django_core.client import (
    Paginated,
    Identifiable,
    ModelWithFiles,
    get_type,
    MemberDetail,
)
from ichec_django_core.models import Member


class BaseResponse(BaseModel):
    status_code: int
    body: dict | None


class ListResponse(BaseModel):
    status_code: int
    body: Paginated | dict | None


class DetailResponse(BaseModel):
    status_code: int
    body: Identifiable | dict | None


class AuthAPITestCase(APITestCase):

    def authenticate(self, username: str):
        user = User.objects.get(username=username)
        self.client.force_authenticate(user=user)

    def deauthenticate(self):
        self.client.force_authenticate(user=None)

    def authenticated_op(self, username: str, op):
        self.authenticate(username)
        response = op()
        self.deauthenticate()
        return response

    def get_member_detail(self, name: str) -> DetailResponse:
        user = User.objects.get(username=name)
        self.authenticate(name)
        resp = self.client.get(f"/api/members/{user.id}/", format="json")
        if resp.status_code < 400:
            response = DetailResponse(
                status_code=resp.status_code, body=MemberDetail(**resp.data)
            )
        else:
            response = DetailResponse(status_code=resp.status_code, body=resp.data)
        self.deauthenticate()
        return response

    def get_member_url(self, name):
        detail = self.get_member_detail(name)
        return detail.body.url

    def get_user(self, name: str) -> Member:
        return Member.objects.get(username=name)

    def get_type(self, action: str):
        path = self.url.split("api/")[1].split("/")[0]
        return get_type(path, action)

    def public_list(self, queries: dict | None = None) -> ListResponse:

        response = self.client.get(self.url, query_params=queries, format="json")

        item_t = self.get_type("list")
        if response.status_code < 400:
            return ListResponse(
                status_code=response.status_code,
                body=Paginated(
                    count=response.data["count"],
                    next=response.data["next"],
                    previous=response.data["previous"],
                    results=[item_t(**r) for r in response.data["results"]],
                ),
            )
        else:
            return ListResponse(status_code=response.status_code, body=response.data)

    def auth_list(self, username: str, queries: dict | None = None) -> ListResponse:
        return self.authenticated_op(username, partial(self.public_list, queries))

    def raw_get(
        self, url: str, queries: dict | None = None, action: str = ""
    ) -> BaseResponse | DetailResponse:

        response = self.client.get(url, query_params=queries)
        if action == "detail":
            return DetailResponse(
                status_code=response.status_code,
                body=self.get_type(action)(**response.data),
            )
        else:
            return BaseResponse(status_code=response.status_code, body=response.data)

    def auth_raw_get(
        self, username: str, url: str, queries: dict | None = None, action: str = ""
    ) -> BaseResponse:
        return self.authenticated_op(
            username, partial(self.raw_get, url, queries, action)
        )

    def get(self, resource_id: int, field: str) -> BaseResponse:
        response = self.client.get(self.url + f"{resource_id}/{field}/")

        if isinstance(response, DownloadResponse):
            body = {"basename": response.basename}
        else:
            body = response.data

        return BaseResponse(status_code=response.status_code, body=body)

    def auth_get(self, username: str, resource_id: int, field: str) -> BaseResponse:
        return self.authenticated_op(username, partial(self.get, resource_id, field))

    def detail(self, resource_id: int, role: str = "") -> DetailResponse:
        response = self.client.get(self.url + f"{resource_id}/", format="json")

        type_str = "detail"
        if role:
            type_str += f"-{role}"
        item_t = self.get_type(type_str)
        if response.status_code < 400:
            return DetailResponse(
                status_code=response.status_code, body=item_t(**response.data)
            )
        else:
            return DetailResponse(status_code=response.status_code, body=response.data)

    def auth_detail(
        self, username: str, resource_id: int, role: str = ""
    ) -> DetailResponse:
        return self.authenticated_op(username, partial(self.detail, resource_id, role))

    def create(self, data, format="json") -> DetailResponse:

        if isinstance(data, ModelWithFiles):
            model = data.model
            # files: dict = data.files
        else:
            model = data
            # files = {}

        response = self.client.post(self.url, model.model_dump(), format=format)
        item_t = self.get_type("detail")
        if response.status_code < 400:
            return DetailResponse(
                status_code=response.status_code, body=item_t(**response.data)
            )
        else:
            return DetailResponse(status_code=response.status_code, body=response.data)

    def auth_create(self, username: str, data, format="json") -> DetailResponse:
        return self.authenticated_op(username, partial(self.create, data, format))

    def update(self, resource_id: int, data, format="json") -> DetailResponse:

        if isinstance(data, ModelWithFiles):
            model = data.model
            # files: dict = data.files
        else:
            model = data
            # files = {}
        response = self.client.put(
            self.url + f"{resource_id}/", model.model_dump(), format=format
        )
        item_t = self.get_type("detail")
        if response.status_code < 400:
            return DetailResponse(
                status_code=response.status_code, body=item_t(**response.data)
            )
        else:
            return DetailResponse(status_code=response.status_code, body=response.data)

    def auth_update(self, username: str, resource_id: int, data) -> DetailResponse:
        return self.authenticated_op(username, partial(self.update, resource_id, data))

    def delete(self, resource_id: int) -> BaseResponse:
        response = self.client.delete(self.url + f"{resource_id}/")
        return BaseResponse(status_code=response.status_code, body=None)

    def auth_delete(self, username: str, resource_id: int) -> BaseResponse:
        return self.authenticated_op(username, partial(self.delete, resource_id))

    def put_file(self, resource_id: int, field: str, data, filename) -> BaseResponse:
        response = self.client.put(
            f"{self.url}{resource_id}/{field}/upload",
            data,
            content_type="application/octet-stream",
            headers={"Content-Disposition": f'attachment; filename="{filename}"'},
        )
        return BaseResponse(status_code=response.status_code, body=response.data)

    def auth_put_file(
        self, username: str, resource_id: int, field: str, data, filename: str
    ) -> BaseResponse:
        return self.authenticated_op(
            username, partial(self.put_file, resource_id, field, data, filename)
        )

    def assert_status(self, response, code, msg=""):
        match code:
            case 200:
                return self.assert_200(response, msg)
            case 201:
                return self.assert_201(response, msg)
            case 204:
                self.assert_204(response, msg)
            case 400:
                self.assert_400(response, msg)
            case 401:
                self.assert_401(response, msg)
            case 403:
                self.assert_403(response, msg)
            case 404:
                self.assert_404(response, msg)

    def assert_200(self, response, msg=""):
        self.assertEqual(
            response.status_code,
            status.HTTP_200_OK,
            msg + " | Response: " + str(response.body),
        )
        return response

    def assert_201(self, response, msg=""):
        self.assertEqual(
            response.status_code,
            status.HTTP_201_CREATED,
            msg + " | Response: " + str(response.body),
        )
        return response

    def assert_204(self, response, msg=""):
        self.assertEqual(
            response.status_code,
            status.HTTP_204_NO_CONTENT,
            msg + " | Response: " + str(response.body),
        )

    def assert_401(self, response, msg=""):
        self.assertEqual(
            response.status_code,
            status.HTTP_401_UNAUTHORIZED,
            msg + " | Response: " + str(response.body),
        )

    def assert_403(self, response, msg=""):
        self.assertEqual(
            response.status_code,
            status.HTTP_403_FORBIDDEN,
            msg + " | Response: " + str(response.body),
        )

    def assert_400(self, response, msg=""):
        self.assertEqual(
            response.status_code,
            status.HTTP_400_BAD_REQUEST,
            msg + " | Response: " + str(response.body),
        )

    def assert_404(self, response, msg=""):
        self.assertEqual(
            response.status_code,
            status.HTTP_404_NOT_FOUND,
            msg + " | Response: " + str(response.body),
        )
