try:
    from StringIO import StringIO  # Python 2
except ImportError:
    from io import StringIO  # Python 3
import os
import shutil
import tempfile

try:
    from unittest import mock
except ImportError:
    import mock

from django.core.management import call_command
from django.db import models
from django.template import Context, Engine, Template
from django.test import TestCase

from flexi_tag.management.commands.generate_tag_models import Command
from flexi_tag.utils.models import FlexiTagMixin


class TestModel(FlexiTagMixin):
    class Meta:
        app_label = "tests"


class SimpleTestModel(FlexiTagMixin):
    name = models.CharField(max_length=100)

    class Meta:
        app_label = "tests"


class NestedTestModel(FlexiTagMixin):
    title = models.CharField(max_length=100)
    parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)

    class Meta:
        app_label = "tests"


try:
    import builtins  # noqa: F401

    open_path = "builtins.open"
except ImportError:
    import __builtin__  # noqa: F401

    open_path = "__builtin__.open"


class GenerateTagModelsCommandTestCase(TestCase):
    def setUp(self):
        self.test_dir = tempfile.mkdtemp()

        self.model_template_string = """
# This file is auto-generated. Do not edit manually.
# Generated by the generate_tag_models command.

from django.db import models
from flexi_tag.utils.compat import JSONField
from django.contrib.postgres.indexes import GinIndex


class {{ model_name }}Tag(models.Model):
    instance = models.OneToOneField(
        "{{ app_label }}.{{ model_name }}",
        on_delete=models.CASCADE,
        primary_key=True,
    )
    tags = JSONField(default=list)

    class Meta:
        app_label = "{{ app_label }}"
        db_table = "{{ app_label }}_{{ model_lower_name }}_tag"
        indexes = [GinIndex(fields=["tags"])]

    def __str__(self):
        return "Tags for {}".format(self.instance)"
"""

    def tearDown(self):
        shutil.rmtree(self.test_dir)

    @mock.patch(
        "flexi_tag.management.commands.generate_tag_models.apps.get_app_configs"
    )
    @mock.patch("flexi_tag.management.commands.generate_tag_models.import_module")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.inspect.getfile")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.Template")
    def test_command_execution(
        self, mock_template, mock_getfile, mock_import_module, mock_get_app_configs
    ):
        mock_model = mock.MagicMock()
        mock_model.__name__ = "MyModel"
        mock_model._meta.app_label = "myapp"
        mock_model.__module__ = "myapp.models"

        mock_model.__bases__ = (FlexiTagMixin,)

        mock_app_config = mock.MagicMock()
        mock_app_config.get_models.return_value = [mock_model]
        mock_get_app_configs.return_value = [mock_app_config]

        mock_module = mock.MagicMock()
        mock_import_module.return_value = mock_module

        test_file_path = os.path.join(self.test_dir, "models.py")
        mock_getfile.return_value = test_file_path

        mock_template_instance = mock.MagicMock()
        mock_template_instance.render.return_value = "# Generated model content"
        mock_template.return_value = mock_template_instance

        out = StringIO()

        call_command("generate_tag_models", stdout=out)

        self.assertIn("Successfully generated all tag models", out.getvalue())

    @mock.patch(
        "flexi_tag.management.commands.generate_tag_models.apps.get_app_configs"
    )
    @mock.patch("flexi_tag.management.commands.generate_tag_models.import_module")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.inspect.getfile")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.Template")
    @mock.patch(open_path, new_callable=mock.mock_open)
    def test_dry_run_option(
        self,
        mock_open,
        mock_template,
        mock_getfile,
        mock_import_module,
        mock_get_app_configs,
    ):
        mock_model = mock.MagicMock()
        mock_model.__name__ = "MyModel"
        mock_model._meta.app_label = "myapp"
        mock_model.__module__ = "myapp.models"

        mock_model.__bases__ = (FlexiTagMixin,)

        mock_app_config = mock.MagicMock()
        mock_app_config.get_models.return_value = [mock_model]
        mock_get_app_configs.return_value = [mock_app_config]

        mock_module = mock.MagicMock()
        mock_import_module.return_value = mock_module

        test_file_path = os.path.join(self.test_dir, "models.py")
        mock_getfile.return_value = test_file_path

        mock_template_instance = mock.MagicMock()
        mock_template_instance.render.return_value = "# Generated model content"
        mock_template.return_value = mock_template_instance

        out = StringIO()
        call_command("generate_tag_models", "--dry-run", stdout=out)

        self.assertIn("Dry run completed", out.getvalue())

        self.assertFalse(mock_open.called)

    @mock.patch("django.template.Engine.get_default")
    def test_template_content(self, mock_get_default):
        engine = Engine()
        mock_get_default.return_value = engine

        template = Template(self.model_template_string)

        context = Context(
            {
                "model_name": "TestModel",
                "app_label": "testapp",
                "model_lower_name": "testmodel",
            }
        )

        rendered_template = template.render(context)

        self.assertIn("class TestModelTag(models.Model):", rendered_template)
        self.assertIn('app_label = "testapp"', rendered_template)
        self.assertIn('db_table = "testapp_testmodel_tag"', rendered_template)

    @mock.patch(
        "flexi_tag.management.commands.generate_tag_models.apps.get_app_configs"
    )
    @mock.patch("flexi_tag.management.commands.generate_tag_models.Template")
    def test_command_with_no_flexi_tag_models(
        self, mock_template, mock_get_app_configs
    ):
        mock_template_instance = mock.MagicMock()
        mock_template_instance.render.return_value = "# Generated model content"
        mock_template.return_value = mock_template_instance

        out = StringIO()

        mock_model = mock.MagicMock()
        mock_model.__name__ = "RegularModel"
        mock_model.__bases__ = (object,)

        mock_app_config = mock.MagicMock()
        mock_app_config.get_models.return_value = [mock_model]
        mock_get_app_configs.return_value = [mock_app_config]

        call_command("generate_tag_models", stdout=out)

        self.assertIn("Successfully generated all tag models", out.getvalue())


class CustomTagGenerationTestCase(TestCase):
    def setUp(self):
        self.test_dir = tempfile.mkdtemp()

    def tearDown(self):
        shutil.rmtree(self.test_dir)

    @mock.patch("flexi_tag.management.commands.generate_tag_models.Template")
    @mock.patch(
        "flexi_tag.management.commands.generate_tag_models.apps.get_app_configs"
    )
    @mock.patch("flexi_tag.management.commands.generate_tag_models.import_module")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.inspect.getfile")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.open", create=True)
    def test_multiple_models_generation(
        self,
        mock_open,
        mock_getfile,
        mock_import_module,
        mock_get_app_configs,
        mock_template_class,
    ):
        mock_template = mock.MagicMock()
        mock_template.render.return_value = "# Mocked template content"
        mock_template_class.return_value = mock_template

        mock_model1 = mock.MagicMock()
        mock_model1.__name__ = "Model1"
        mock_model1._meta.app_label = "testapp"
        mock_model1.__module__ = "testapp.models"
        mock_model1.__bases__ = (FlexiTagMixin,)

        mock_model2 = mock.MagicMock()
        mock_model2.__name__ = "Model2"
        mock_model2._meta.app_label = "testapp"
        mock_model2.__module__ = "testapp.models"
        mock_model2.__bases__ = (FlexiTagMixin,)

        mock_app_config = mock.MagicMock()
        mock_app_config.get_models.return_value = [mock_model1, mock_model2]
        mock_get_app_configs.return_value = [mock_app_config]

        mock_module = mock.MagicMock()
        mock_import_module.return_value = mock_module

        test_file_path = os.path.join(self.test_dir, "models.py")
        mock_getfile.return_value = test_file_path

        mock_file = mock.MagicMock()
        mock_open.return_value.__enter__.return_value = mock_file

        out = StringIO()

        call_command("generate_tag_models", stdout=out)

        self.assertEqual(mock_open.call_count, 2)
        self.assertEqual(mock_file.write.call_count, 2)
        self.assertIn("Successfully generated all tag models", out.getvalue())

    @mock.patch("flexi_tag.management.commands.generate_tag_models.Template")
    @mock.patch(
        "flexi_tag.management.commands.generate_tag_models.apps.get_app_configs"
    )
    @mock.patch("flexi_tag.management.commands.generate_tag_models.import_module")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.inspect.getfile")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.open", create=True)
    def test_model_with_special_name(
        self,
        mock_open,
        mock_getfile,
        mock_import_module,
        mock_get_app_configs,
        mock_template_class,
    ):
        mock_template = mock.MagicMock()
        mock_template.render.return_value = "# Mocked template content"
        mock_template_class.return_value = mock_template

        mock_model = mock.MagicMock()
        mock_model.__name__ = "Special_Model_Name-With.Characters$"
        mock_model._meta.app_label = "testapp"
        mock_model.__module__ = "testapp.models"
        mock_model.__bases__ = (FlexiTagMixin,)

        mock_app_config = mock.MagicMock()
        mock_app_config.get_models.return_value = [mock_model]
        mock_get_app_configs.return_value = [mock_app_config]

        mock_module = mock.MagicMock()
        mock_import_module.return_value = mock_module

        test_file_path = os.path.join(self.test_dir, "models.py")
        mock_getfile.return_value = test_file_path

        mock_file = mock.MagicMock()
        mock_open.return_value.__enter__.return_value = mock_file

        out = StringIO()

        call_command("generate_tag_models", stdout=out)

        self.assertEqual(mock_open.call_count, 1)
        self.assertEqual(mock_file.write.call_count, 1)
        self.assertEqual(mock_template.render.call_count, 1)

        context = mock_template.render.call_args[0][0]
        self.assertEqual(context["model_name"], "Special_Model_Name-With.Characters$")
        self.assertEqual(
            context["model_lower_name"], "special_model_name-with.characters$"
        )
        self.assertIn("Successfully generated all tag models", out.getvalue())

    @mock.patch("flexi_tag.management.commands.generate_tag_models.Template")
    @mock.patch(
        "flexi_tag.management.commands.generate_tag_models.apps.get_app_configs"
    )
    @mock.patch("flexi_tag.management.commands.generate_tag_models.import_module")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.inspect.getfile")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.open", create=True)
    def test_command_with_non_django_class(
        self,
        mock_open,
        mock_getfile,
        mock_import_module,
        mock_get_app_configs,
        mock_template_class,
    ):
        mock_template = mock.MagicMock()
        mock_template.render.return_value = "# Mocked template content"
        mock_template_class.return_value = mock_template

        mock_model = mock.MagicMock()
        mock_model.__name__ = "NonModelClass"
        mock_model.__bases__ = (FlexiTagMixin,)
        mock_model._meta = mock.MagicMock()
        mock_model._meta.app_label = "testapp"
        mock_model.__module__ = "testapp.models"

        mock_module = mock.MagicMock()
        mock_import_module.return_value = mock_module

        test_file_path = os.path.join(self.test_dir, "models.py")
        mock_getfile.return_value = test_file_path

        mock_file = mock.MagicMock()
        mock_open.return_value.__enter__.return_value = mock_file

        mock_app_config = mock.MagicMock()
        mock_app_config.get_models.return_value = [mock_model]
        mock_get_app_configs.return_value = [mock_app_config]

        out = StringIO()

        call_command("generate_tag_models", stdout=out)

        self.assertIn("Successfully generated all tag models", out.getvalue())

    @mock.patch("sys.exit")
    def test_command_help(self, mock_exit):
        command = Command()
        command.stdout = StringIO()

        help_text = command.help

        self.assertIn("Generate tag models", help_text)
        self.assertTrue(callable(command.add_arguments))

        parser = mock.MagicMock()
        command.add_arguments(parser)
        self.assertTrue(parser.add_argument.called)
        args, _ = parser.add_argument.call_args
        self.assertEqual(args[0], "--dry-run")

        mock_exit.assert_not_called()

    @mock.patch("flexi_tag.management.commands.generate_tag_models.Template")
    @mock.patch(
        "flexi_tag.management.commands.generate_tag_models.apps.get_app_configs"
    )
    @mock.patch("flexi_tag.management.commands.generate_tag_models.import_module")
    @mock.patch("flexi_tag.management.commands.generate_tag_models.inspect.getfile")
    def test_dry_run_option(
        self,
        mock_getfile,
        mock_import_module,
        mock_get_app_configs,
        mock_template_class,
    ):
        mock_template = mock.MagicMock()
        mock_template.render.return_value = "# Mocked template content"
        mock_template_class.return_value = mock_template

        mock_model = mock.MagicMock()
        mock_model.__name__ = "TestModel"
        mock_model._meta.app_label = "testapp"
        mock_model.__module__ = "testapp.models"
        mock_model.__bases__ = (FlexiTagMixin,)

        mock_app_config = mock.MagicMock()
        mock_app_config.get_models.return_value = [mock_model]
        mock_get_app_configs.return_value = [mock_app_config]

        mock_module = mock.MagicMock()
        mock_import_module.return_value = mock_module

        test_file_path = os.path.join(self.test_dir, "models.py")
        mock_getfile.return_value = test_file_path

        out = StringIO()

        call_command("generate_tag_models", "--dry-run", stdout=out)

        output = out.getvalue()
        self.assertIn("Would create file:", output)
        self.assertIn("Dry run completed. No files were created.", output)
        self.assertNotIn("Successfully generated all tag models", output)
