import unittest

import django.db.utils
from django import VERSION
from django.db import connections, connection, models
from django.db.models.functions import Now
from django.test import TransactionTestCase, TestCase, skipUnlessDBFeature
from django.utils import timezone

from ..models import Author, BinaryData


class TestTableWithTrigger(TransactionTestCase):
    def test_insert_into_table_with_trigger(self):
        connection = connections['default']
        with connection.schema_editor() as cursor:
            cursor.execute("""
                CREATE TRIGGER TestTrigger
                ON [testapp_author]
                FOR INSERT
                AS
                INSERT INTO [testapp_editor]([name]) VALUES ('Bar')
            """)

        try:
            # Change can_return_rows_from_bulk_insert to be the same as when
            # has_trigger = True
            old_return_rows_flag = connection.features_class.can_return_rows_from_bulk_insert
            connection.features_class.can_return_rows_from_bulk_insert = False
            Author.objects.create(name='Foo')
        except django.db.utils.ProgrammingError as e:
            self.fail('Check for regression of issue #130. Insert with trigger failed with exception: %s' % e)
        finally:
            with connection.schema_editor() as cursor:
                cursor.execute("DROP TRIGGER TestTrigger")
            connection.features_class.can_return_rows_from_bulk_insert = old_return_rows_flag


class TestBinaryfieldGroupby(TestCase):
    def test_varbinary(self):
        with connection.cursor() as cursor:
            cursor.execute(f"SELECT binary FROM {BinaryData._meta.db_table} WHERE binary = %s GROUP BY binary", [bytes("ABC", 'utf-8')])


@skipUnlessDBFeature("supports_expression_defaults")
class DbDefaultBulkCreateRegressionTests(TransactionTestCase):
    """Regression tests for Django 6.0 db_default bulk insert alignment.

    Django 6.0 introduced DatabaseDefault sentinel values for fields with
    db_default. Our SQLInsertCompiler.as_sql() override must correctly:
      - Exclude a db_default column from the INSERT column list when *every*
        row uses the database default (letting the server supply the value).
      - Include the column when at least one row supplies an explicit value
        (mixing explicit values with DEFAULT keyword or prepared db_default).

    These tests create a raw table with a server-side DEFAULT SYSDATETIME()
    to exercise both paths end-to-end against SQL Server.
    """

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        # This feature is Django 6.0+ only; skip cleanly on older versions.
        # Note: raise unittest.SkipTest, not cls.skipTest(), because
        # skipTest() is an instance method and cannot be called from
        # setUpClass (a classmethod).
        if VERSION < (6, 0):
            raise unittest.SkipTest(
                "db_default bulk insert alignment is Django 6.0+ specific"
            )

        # Unmanaged model so Django doesn't try to create/drop the table
        # via migrations — we handle that with raw SQL below.
        class DbDefaultBulkInsertModel(models.Model):
            name = models.CharField(max_length=100)
            created_at = models.DateTimeField(db_default=Now())

            class Meta:
                app_label = "testapp"
                db_table = "testapp_dbdefault_bulk_insert"
                managed = False

        cls.DbDefaultBulkInsertModel = DbDefaultBulkInsertModel

        with connection.cursor() as cursor:
            cursor.execute(
                """
                IF OBJECT_ID('testapp_dbdefault_bulk_insert', 'U') IS NOT NULL
                    DROP TABLE testapp_dbdefault_bulk_insert
                """
            )
            cursor.execute(
                """
                CREATE TABLE testapp_dbdefault_bulk_insert (
                    id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
                    name NVARCHAR(100) NOT NULL,
                    created_at DATETIME2 NOT NULL DEFAULT SYSDATETIME()
                )
                """
            )

    @classmethod
    def tearDownClass(cls):
        if VERSION < (6, 0):
            # Table was never created when the test was skipped.
            super().tearDownClass()
            return
        with connection.cursor() as cursor:
            cursor.execute(
                """
                IF OBJECT_ID('testapp_dbdefault_bulk_insert', 'U') IS NOT NULL
                    DROP TABLE testapp_dbdefault_bulk_insert
                """
            )
        super().tearDownClass()

    def test_db_default_field_excluded_and_included(self):
        """Verify created_at column presence in INSERT SQL for db_default.

        Case 1 — all rows use the database default:
          The column should be omitted from the INSERT column list so the
          server-side DEFAULT kicks in.  The quoted column name appears only
          in the OUTPUT INSERTED clause (if can_return_rows_from_bulk_insert)
          or not at all.

        Case 2 — at least one row supplies an explicit value:
          The column must appear in the INSERT column list (so the explicit
          value is written) *and* in OUTPUT INSERTED (if applicable).
        """
        model = self.DbDefaultBulkInsertModel
        created_at_quoted_name = connection.ops.quote_name("created_at")

        # --- Case 1: all rows rely on db_default for created_at ---
        with self.assertNumQueries(1) as ctx:
            model.objects.bulk_create([model(name="foo"), model(name="bar")])

        # created_at should NOT be in the INSERT column list.
        # It appears once only if OUTPUT INSERTED includes it.
        self.assertEqual(
            ctx[0]["sql"].count(created_at_quoted_name),
            1 if connection.features.can_return_rows_from_bulk_insert else 0,
        )

        # --- Case 2: one row overrides created_at with an explicit value ---
        with self.assertNumQueries(1) as ctx:
            model.objects.bulk_create(
                [
                    model(name="baz", created_at=timezone.now()),
                    model(name="qux"),
                ]
            )

        # created_at must be in the INSERT column list (1 occurrence)
        # plus OUTPUT INSERTED (1 more if can_return_rows_from_bulk_insert).
        self.assertEqual(
            ctx[0]["sql"].count(created_at_quoted_name),
            2 if connection.features.can_return_rows_from_bulk_insert else 1,
        )

    def test_single_insert_with_returning_fields_when_bulk_rows_unsupported(self):
        """Single-row create must still return all db_returning fields.

        Regression for a path where can_return_rows_from_bulk_insert=False
        caused SQLInsertCompiler to use SCOPE_IDENTITY() (1 column) while
        Django expected all returning fields, leading to IndexError.
        """
        model = self.DbDefaultBulkInsertModel
        old_return_rows_flag = connection.features_class.can_return_rows_from_bulk_insert
        connection.features_class.can_return_rows_from_bulk_insert = False
        try:
            with self.assertNumQueries(1) as ctx:
                obj = model.objects.create(name="single")

            self.assertIsNotNone(obj.pk)
            self.assertIsNotNone(obj.created_at)
            self.assertIn("OUTPUT INSERTED", ctx[0]["sql"])
            self.assertNotIn("SCOPE_IDENTITY", ctx[0]["sql"])
        finally:
            connection.features_class.can_return_rows_from_bulk_insert = old_return_rows_flag
