# -*- coding: utf-8 -*-
"""_____________________________________________________________________

:PROJECT: LARAsuite

*generate unittests *

:details: generate_grpc_client - a unittests generator for LARA-django.
          This is a LARA-django-base management command to generate a unittests for a
          given app. The generated client is a python module that can be used to
          communicate with the gRPC server of the given app.

          requirements:
                python-lorem
:usage: lara-django-dev generate_unittests --author-name "mark doerr" --author-email "mark.doerr@uni-greifswald.de" --force lara_django_base

:authors: mark doerr <mark.doerr@uni-greifswald.de>

.. note:: -
.. todo:: - https://stackoverflow.com/questions/11380413/python-unittest-passing-arguments
          - speed improvemnts https://medium.com/@thehackadda/tips-for-speeding-up-your-django-tests-35bab8250760
________________________________________________________________________
"""

import re
import os
import logging
import lorem  # pip install python-lorem
import random
import uuid
from datetime import datetime
import json

from django.apps import apps
from django.conf import settings
from django.core.management.base import LabelCommand, CommandError
from django.db import models


# Configurable constants
MAX_LINE_WIDTH = getattr(settings, "MAX_LINE_WIDTH", 120)
INDENT_WIDTH = getattr(settings, "INDENT_WIDTH", 4)

UNITTEST_FILE_HEADER = """\"\"\"_____________________________________________________________________

:PROJECT: LARAsuite

*{lara_app} unittests *

:details: {lara_app} unittests.
         - generated with 'lara-django-dev generate_unittests {lara_app}'
:authors: {lara_author} <{lara_author_email}>

.. note:: -
.. todo:: - 
________________________________________________________________________
\"\"\""""


def indent(level=1, text="", end="\n"):
    return "\n".join([(level * INDENT_WIDTH * " ") + line for line in text.split("\n")]) + end


def lower_strip(s):
    return s.lower().replace(" ", "").strip()


class Command(LabelCommand):
    help = """Generate a unittests for a given LARA app (models) - 1a"""
    args = "[app_name, author_name, author_email]"
    can_import_settings = True

    def add_arguments(self, parser):
        parser.add_argument("app_name", help="Name of the LARA app to generate the unittests for")

        parser.add_argument("--author-name", help="Name of the author", default="lara")
        parser.add_argument("--author-email", help="Email of the author", default="lara@lara.org")
        parser.add_argument("--force", action="store_true", help="overwrite existing files", default=False)
        # parser.add_argument('model_name', nargs='*')

    # @signalcommand
    def handle(self, *args, **options):
        self.app_name = options["app_name"]

        logging.warning("!! only a scaffold is generated, please check/add content to the generated files !!!")

        try:
            app = apps.get_app_config(self.app_name)
        except LookupError:
            self.stderr.write("This command requires an existing app name as argument")
            self.stderr.write("Available apps:")
            app_labels = [app.label for app in apps.get_app_configs()]
            for label in sorted(app_labels):
                self.stderr.write("    %s" % label)
            return

        model_res = []
        # for arg in options['model_name']:
        #     model_res.append(re.compile(arg, re.IGNORECASE))

        AppUnittestGenerator(app, model_res, options)

        # self.stdout.write()


class AppUnittestGenerator:
    def __init__(self, app_config, model_res, options):
        self.app_config = app_config
        self.model_res = model_res

        self.app_name = options["app_name"]
        self.author_name = options["author_name"]
        self.author_email = options["author_email"]
        self.force_overwrite = options["force"]

        self.model_dict = {}
        self.internal_model_name = set()
        self.external_model_name = set()

        self.unittests_str = ""
        self.model_names = [model.__name__ for model in self.app_config.get_models()]

        self.generate_unittests()

    def generate_unittests(self):
        self.generate_unittests_header()
        self.generate_unittests_imports()
        self.generate_model_tests()
        self.generate_unittests_main()

        # save unittests to file
        unittest_filename = f"test_{self.app_name}.py"

        print(f"writing {unittest_filename} to {os.getcwd()}")
        print(self.unittests_str)

        if os.path.isfile(unittest_filename):
            if self.force_overwrite:
                with open(unittest_filename, "w") as f:
                    f.write(self.unittests_str)
            else:
                append = input(
                    f"{unittest_filename} already exists,\n press 'a' to append,\n press 'o' to overwrite,\n or 'n' to do nothing: "
                )
                if append.lower() == "a":
                    with open(unittest_filename, "a") as f:
                        f.write(self.unittests_str)
                elif append.lower() == "o":
                    with open(unittest_filename, "w") as f:
                        f.write(self.unittests_str)
        else:
            with open(unittest_filename, "w") as f:
                f.write(self.unittests_str)

    def generate_unittests_header(self):
        self.unittests_str += UNITTEST_FILE_HEADER.format(
            lara_app=self.app_name,
            lara_author=self.author_name,
            lara_author_email=self.author_email,
        )

    def generate_unittests_imports(self):

        self.unittests_str += f"""

#import asyncio
import json
import logging
import uuid
from unittest import skip
from django.test import TestCase
from django.utils import timezone
from django.conf import settings


from {self.app_name}.models import ( {', '.join(self.model_names)}\n)\n

TEST_NUM_INSTANCES = 1  # TODO: set the number via command line argument\n\n
"""

    def generate_model_tests(self):

        unittests_handler_str = ""

        for model in self.app_config.get_models():

            self.unittests_str += f"""#_________________  {model.__name__}  ______________________\n

@skip("skip {model.__name__} tests, because they are not implemented yet.")
class {model.__name__.capitalize()}CRUDTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()
        # setting up the model data in the test database
        cls.{model.__name__.lower()}_model = [
            {model.__name__}.create({ self.parse_fields_for_init(model) }
        ) for i in range(TEST_NUM_INSTANCES) ]


    def setUp(self):
        # setting up the test environment
        pass

     def test_create_{model.__name__.lower()}(self):
        # testing creating objects from the {model.__name__} model

        #cat = Animal.objects.get(name="cat")
        #self.assertEqual(lion.speak(), 'The lion says "roar"')
        pass
              
    def test_retrieve_{model.__name__.lower()}(self):
        # testing retrieving objects from the {model.__name__} model

        #cat = Animal.objects.get(name="cat")
        #self.assertEqual(lion.speak(), 'The lion says "roar"')
        pass
    
    def test_update_{model.__name__.lower()}(self):
        # testing updating objects from the {model.__name__} model

        #self.assertEqual(lion.speak(), 'The lion says "roar"')
        pass

    def test_delete_{model.__name__.lower()}(self):
        # testing deleting objects from the {model.__name__} model
        pass\n\n"""

    def parse_fields_for_init(self, model):
        fields_str = "\n"

        default_prefix = "lara"

        for field in model._meta.get_fields():
            # print(f"field: {field} --> {type(field)} \n")

            if (
                isinstance(field, models.fields.related.ManyToManyField)
                or isinstance(field, models.fields.reverse_related.ManyToOneRel)
                or isinstance(field, models.fields.reverse_related.ManyToManyRel)
                or isinstance(field, models.fields.reverse_related.OneToOneRel)
                or isinstance(field, models.fields.reverse_related.ManyToManyRel)
            ):
                required = False
            else:
                required = field.blank == False

            match field:
                case models.fields.related.ManyToOneRel():
                    pass
                case models.fields.UUIDField():
                    if field.primary_key:
                        fields_str += indent(4, f"# {field.name} =    # (primary key) {field.help_text}")
                    else:
                        fields_str += indent(4, f"{field.name} = {uuid.uuid4()},   # (UUID) {field.help_text}")
                    # self.external_model_name.add("UUID")

                case models.fields.BooleanField():
                    fields_str += indent(
                        4, f"{field.name} = {random.choice(('True', 'False') ) },   # (bool) {field.help_text}"
                    )
                case models.fields.URLField():
                    url_str = f"http://www.{lorem.get_word()}.org"
                    if field.name == "url":
                        url_str = f"http://www.{lorem.get_word()}.org"
                    elif field.name == "iri":
                        url_str = f"http://w3id.org/lara/{model.__name__}_{{i}}"

                    fields_str += indent(4, f'{field.name} = f"{url_str}",   # (URL - str) {field.help_text}')
                case models.fields.CharField():
                    fields_str += indent(4, f'{field.name} = "{lorem.get_word()}",   # (str) {field.help_text}')

                case models.fields.TextField():
                    if field.name == "name" or field.name == "title":
                        fields_str += indent(4, f'{field.name} = f"{lorem.get_word()}_{{i}}",   # (str) {field.help_text}')
                    else:
                        fields_str += indent(4, f'{field.name} = "{lorem.get_sentence()}",   # (str) {field.help_text}')

                case models.fields.IntegerField():
                    fields_str += indent(4, f"{field.name} = {random.randint(1,100)},   # (int) {field.help_text}")

                case models.fields.FloatField():

                    fields_str += indent(
                        4, f"{field.name} = {random.uniform(1.0, 100.0)},   # (float) {field.help_text}"
                    )

                case models.fields.DecimalField():
                    fields_str += indent(
                        4, f"{field.name} = {random.uniform(1.0, 100.0)},   # (float) {field.help_text}"
                    )

                case models.fields.DateTimeField():
                    fields_str += indent(4, f'{field.name} = "{datetime.now()}",   # (datetime) {field.help_text}')

                case models.fields.DateField():
                    fields_str += indent(4, f'{field.name} = "{datetime.now().date()}",   # (date) {field.help_text}')

                case models.fields.json.JSONField():
                    # random json
                    rand_json = json.dumps(
                        {
                            "key1": lorem.get_word(),
                            "key2": lorem.get_word(),
                            "key3": lorem.get_word(),
                        }
                    )
                    fields_str += indent(4, f"{field.name} = {rand_json},   # (str) {field.help_text}")

                case models.fields.files.FileField():
                    fields_str += indent(4, f'{field.name} = "my_file.md",  # (file) {field.help_text}')

                case models.fields.related.ForeignKey():
                    # print(f"{field.name} --- attributes: {field.__dict__}")
                    fields_str += indent(
                        4, f"{field.name} =  ,  # (ForeignKey ->  {field.related_model.__name__}) {field.help_text}"
                    )
                    self.external_model_name.add(field.related_model.__name__)

                    self.model_dict[field.related_model.__name__] = field.related_model

                case models.OneToOneField():
                    fields_str += indent(
                        4, f"{field.name} =  ,  # (OneToOneField -> {field.related_model.__name__}) {field.help_text}"
                    )
                    self.external_model_name.add(field.related_model.__name__)

                    self.model_dict[field.related_model.__name__] = field.related_model

                case models.fields.related.ManyToManyField():
                    # print(f"field attributes: {field.__dict__}")
                    fields_str += indent(
                        4, f"{field.name} =  ,  # (ManyToManyField -> {field.related_model.__name__}) {field.help_text}"
                    )
                    self.external_model_name.add(field.related_model.__name__)

                    self.model_dict[field.related_model.__name__] = field.related_model

        return fields_str

    def get_relational_fields(self, model):
        foreignkey_fields = set()
        related_names = set()
        m2m_fields = set()
        one2one_fields = set()

        for field in model._meta.get_fields():
            print(f"field: {field} --> {type(field)} ")
            if isinstance(field, models.ForeignKey):
                foreignkey_fields.add(field.name)
            if isinstance(field, models.fields.related.ForeignKey):
                pass
                # print(f"field attributes: {field.__dict__}")
                # related_names.add(field.name)
            if isinstance(field, models.fields.related.ManyToManyField):
                # print(f"field attributes: {field.__dict__}")
                m2m_fields.add(field.name)
            if isinstance(field, models.fields.reverse_related.ManyToManyRel):
                # print(f"field attributes: {field.__dict__}")
                related_names.add(field.name)
                related_names.add(field.related_name)
                related_names.add(field.related_query_name)
                related_names.add(field.field.name)
                # related_names.add(field.field.related_name)
                related_names.add(field.field.related_query_name)
            if isinstance(field, models.fields.related.OneToOneField):
                one2one_fields.add(field.name)
            if isinstance(field, models.fields.reverse_related.OneToOneRel):
                related_names.add(field.name)
                related_names.add(field.related_name)
                related_names.add(field.related_query_name)
        return [foreignkey_fields, m2m_fields, one2one_fields, related_names]

    def generate_unittests_main(self):
        self.unittests_str += f"""
# async def main():
    # (async) main function
    

if __name__ == "__main__":
    # parse command line arguments
    import argparse

    # add num test instances as command line argument
    parser = argparse.ArgumentParser(description="unittests for {self.app_name}")
    parser.add_argument("--verbose", help="verbose output", action="store_true")
    parser.add_argument("--num-instances", help="number of test instances", type=int, default=1)

    args = parser.parse_args()

    # set the number of test instances
    TEST_NUM_INSTANCES = args.num_instances

    # run the unittests
    # asyncio.run(main())
"""
