import inspect
import logging
import sys
import unittest
from logging.config import dictConfig
from math import floor

from yaml import safe_load

from dbrepo.api import dto

logging.addLevelName(level=logging.NOTSET, levelName='TRACE')
logging.basicConfig(level=logging.DEBUG)

# logging configuration
dictConfig({
    'version': 1,
    'formatters': {
        'default': {
            'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
        },
        'simple': {
            'format': '[%(asctime)s] %(levelname)s: %(message)s',
        },
    },
    'handlers': {'console': {
        'class': 'logging.StreamHandler',
        'stream': 'ext://sys.stdout',
        'formatter': 'simple'  # default
    }},
    'root': {
        'level': 'DEBUG',
        'handlers': ['console']
    }
})


class DtoUnitTest(unittest.TestCase):
    schemas = None
    models: [()] = []
    exclusions: [str] = ['EnumType']
    found: int = 0
    skipped: [str] = []

    def setUp(self):
        with open('../../.docs/.openapi/api.yaml') as fh:
            self.schemas = safe_load(fh)['components']['schemas']
            for name, obj in inspect.getmembers(sys.modules[dto.__name__]):
                self.found += 1
                if not inspect.isclass(obj):
                    continue
                if f'{name}Dto' not in self.schemas or name not in self.exclusions:
                    logging.debug(f'skip model {name}: OpenAPI schema definition {name}Dto not found')
                    self.skipped.append(f'{name}Dto')
                    continue
                self.models.append((name, obj))

    def build_model(self, name: str, obj: any, definition: any) -> dict:
        model_dict = dict()
        for property in definition['properties']:
            if 'example' in definition['properties'][property]:
                if '$ref' not in definition['properties'][property]:
                    model_dict[property] = definition['properties'][property]['example']
                    continue
                ref = definition['properties'][property]['$ref'][len('#/components/schemas/'):-3]
                # recursive call
                model_dict[property] = self.build_model(ref, self.get_model(ref), self.schemas[f'{ref}Dto'])
                continue
            if 'items' in definition['properties'][property]:
                if '$ref' not in definition['properties'][property]['items']:
                    continue
                ref = definition['properties'][property]['items']['$ref'][len('#/components/schemas/'):-3]
                # recursive call
                model_dict[property] = [self.build_model(ref, self.get_model(ref), self.schemas[f'{ref}Dto'])]
                continue
            if '$ref' in definition['properties'][property]:
                ref = definition['properties'][property]['$ref'][len('#/components/schemas/'):-3]
                # recursive call
                model_dict[property] = self.build_model(ref, self.get_model(ref), self.schemas[f'{ref}Dto'])
        return model_dict

    def get_model(self, ref: str):
        for name, obj in self.models:
            if name == ref:
                return obj
        return None

    def test_dtos_succeeds(self):
        logging.info(f'Found {self.found} model(s) in {dto.__name__}')
        for name, obj in self.models:
            logging.debug(f'building model: {name} against OpenAPI schema definition {name}Dto')
            model = obj(**self.build_model(name, obj, self.schemas[f'{name}Dto']))
        logging.warning(f'Unable to find {len(self.skipped)} OpenAPI schema definition(s): {self.skipped}')
        logging.info(f'Coverage: {floor((1 - len(self.skipped) / self.found) * 100)}%')
        pass