import inspect
import io
import json
import logging
from ipaddress import ip_address
from typing import Optional, Callable, Union
from unittest import mock

import aiohttp
from aiohttp import web
from aiohttp.test_utils import make_mocked_request as _make_mocked_request
from peewee import SqliteDatabase

from slim import Application, ALL_PERMISSION
from slim.base._view.abstract_sql_view import AbstractSQLView
from slim.base.types.beacon import BeaconInfo
from slim.base.user import BaseUser
from slim.base.view import BaseView
from slim.support.peewee import PeeweeView


def new_app(permission=ALL_PERMISSION, log_level=logging.WARN, **kwargs) -> Application:
    """
    Get application instance
    :param permission:
    :param log_level:
    :param kwargs:
    :return:
    """
    app = Application(cookies_secret=b'123456', permission=permission, log_level=log_level, **kwargs)
    return app


class _MockResponse:
    def __init__(self, headers, content):
        self.headers = headers
        self.content = content

    async def release(self):
        pass


class _MockStream:
    def __init__(self, content):
        self.content = io.BytesIO(content)

    async def read(self, size=None):
        return self.content.read(size)

    def at_eof(self):
        return self.content.tell() == len(self.content.getbuffer())

    async def readline(self):
        return self.content.readline()

    def unread_data(self, data):
        self.content = io.BytesIO(data + self.content.read())


def _polyfill_post(request: web.Request, post):
    if post:
        if isinstance(post, bytes):
            request._read_bytes = post
        else:
            try:
                request._read_bytes = bytes(json.dumps(post), 'utf-8')
            except TypeError:
                # logger.warning(...)
                pass

        if request.content_type == 'multipart/form-data':
            resp = _MockResponse(request.headers, _MockStream(post))
            mr = aiohttp.MultipartReader.from_response(resp)

            async def multipart():
                return mr

            request.multipart = multipart

    else:
        request._read_bytes = b''


def get_peewee_db():
    """
    Get peewee database instance
    :return:
    """
    db = SqliteDatabase(":memory:")
    return db


async def make_mocked_view_instance(app, view_cls, method, url, params=None, post=None, *, headers=None,
                                    content_type='application/json') -> Union[BaseView, AbstractSQLView, PeeweeView]:
    if not headers:
        headers = {}

    if content_type:
        headers['Content-Type'] = content_type

    request = _make_mocked_request(method, url, headers=headers, protocol=mock.Mock(), app=app)
    _polyfill_post(request, post)

    view = view_cls(app, request)
    view._params_cache = params
    view._post_data_cache = post

    await view._prepare()
    return view


async def invoke_interface(app: Application, func: [Callable], params=None, post=None, *, headers=None, method=None,
                           user=None, bulk=False, returning=None, role=None, content_type='application/json',
                           fill_post_cache=True) -> Optional[BaseView]:
    """
    :param app:
    :param func:
    :param params:
    :param post:
    :param headers:
    :param method: auto detect
    :param user:
    :param bulk:
    :param returning:
    :param role:
    :param content_type:
    :param fill_post_cache:
    :return:
    """
    url = 'mock_url'

    if user:
        assert isinstance(user, BaseUser), 'user must be a instance of `BaseUser`'
    assert inspect.ismethod(func), 'func must be method. e.g. UserView().get'
    view_cls = func.__self__.__class__
    func = func.__func__

    beacon_info_dict = app.route._handler_to_beacon_info.get(func)
    beacon_info: BeaconInfo = beacon_info_dict.get(view_cls) if beacon_info_dict else None

    if beacon_info:
        beacon_func = beacon_info.beacon_func
        info = app.route._beacons[beacon_func]
        url = info.route.fullpath
        _method = next(iter(info.route.method))

        if method:
            _method = method

        headers = headers or {}
        if bulk:
            headers['bulk'] = bulk
        if role:
            headers['role'] = role
        if returning:
            headers['returning'] = 'true'

        if content_type:
            headers['Content-Type'] = content_type

        request = _make_mocked_request(_method, url, headers=headers, protocol=mock.Mock(), app=app)
        _polyfill_post(request, post)

        view_ref: Optional[BaseView] = None

        def hack_view(view: BaseView):
            nonlocal view_ref
            view_ref = view
            view._params_cache = params
            if fill_post_cache:
                view._post_data_cache = post
            view._ip_cache = ip_address('127.0.0.1')
            view._current_user = user

        await app._request_solver(request, beacon_func, hack_view)
        assert view_ref, 'Invoke interface failed. Did you call `app._prepare()`?'
        return view_ref