"""All public technical paths plus representative shared applicability races."""
import uuid
from unittest.mock import patch

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.db.models.deletion import ProtectedError
from django.test import TestCase, TransactionTestCase
from django.urls import reverse

from apps.access.authorization import is_authorized
from apps.catalog.models import ProductCategory
from apps.catalog import services as catalog_services
from apps.organization import test_assignment_concurrency as concurrency
from . import models, services, queries

# Explicit public paths; tests share assertions without generating model classes.
CASES = (
    (models.FaultDiagnosis, models.FaultDiagnosisProductCategory, "fault_diagnosis", "diagnosis",
     "set_fault_diagnosis_applicability", "applicable_fault_diagnoses_for_category", "fault_diagnosis_applies_to_category",
     "deactivate_fault_diagnosis", "reactivate_fault_diagnosis"),
    (models.RootCause, models.RootCauseProductCategory, "root_cause", "root_cause",
     "set_root_cause_applicability", "applicable_root_causes_for_category", "root_cause_applies_to_category",
     "deactivate_root_cause", "reactivate_root_cause"),
    (models.RepairAction, models.RepairActionProductCategory, "repair_action", "repair_action",
     "set_repair_action_applicability", "applicable_repair_actions_for_category", "repair_action_applies_to_category",
     "deactivate_repair_action", "reactivate_repair_action"),
)


class TechnicalTaxonomyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.category = ProductCategory.objects.create(code="A", name="A")
        cls.other = ProductCategory.objects.create(code="B", name="B")
        cls.unmapped = ProductCategory.objects.create(code="C", name="C")
        for model, *_ in CASES:
            model.objects.create(code="Z_TARGET", name="Target")

    def record(self, case):
        return case[0].objects.get(code="Z_TARGET")

    def configure(self, case, categories=(), global_mode=False):
        return getattr(services, case[4])(**{case[3]: self.record(case)},
            applies_to_all_product_categories=global_mode, product_categories=categories)

    def applies(self, case, category=None):
        return getattr(queries, case[6])(**{case[3]: self.record(case)},
            product_category=self.category if category is None else category)

    def test_master_identity_defaults_and_independence(self):
        for case in CASES:
            obj = self.record(case)
            with self.subTest(model=case[0].__name__):
                self.assertIsInstance(obj.pk, uuid.UUID)
                self.assertIsNotNone(obj.created_at)
                self.assertIsNotNone(obj.updated_at)
                self.assertTrue(obj.is_active)
                self.assertFalse(obj.applies_to_all_product_categories)
                self.assertEqual(obj.description, "")
                self.assertFalse(any(field.many_to_one for field in obj._meta.fields))
                self.assertFalse(self.applies(case))

    def test_codes_normalize_and_allow_correction_preserving_text(self):
        for case in CASES:
            obj = self.record(case)
            before = obj.updated_at
            obj.code, obj.name, obj.description = " corrected-1 ", "Mixed Name", "Mixed Description"
            obj.save(update_fields=["code", "name", "description"])
            obj.refresh_from_db()
            self.assertEqual(obj.code, "CORRECTED-1")
            self.assertEqual(obj.name, "Mixed Name")
            self.assertEqual(obj.description, "Mixed Description")
            self.assertGreater(obj.updated_at, before)

    def test_required_fields_and_case_equivalent_uniqueness(self):
        for model, *_ in CASES:
            for code, name in [(" z_target ", "Duplicate"), ("", "Empty"), ("NEW", ""), ("BAD CODE", "Bad")]:
                with self.subTest(model=model.__name__, code=code), self.assertRaises(ValidationError):
                    model.objects.create(code=code, name=name)

    def test_database_code_and_unique_constraints(self):
        for model, *_ in CASES:
            for code in ["Z_TARGET", "lower", " BAD", "_BAD", ""]:
                with self.subTest(model=model.__name__, code=code):
                    with self.assertRaises(IntegrityError), transaction.atomic():
                        model.objects.bulk_create([model(code=code, name="Invalid")])

    def test_each_public_service_uses_shared_atomic_boundary(self):
        for case in CASES:
            with self.subTest(model=case[0].__name__):
                with patch.object(services, "_set_applicability", wraps=services._set_applicability) as operation:
                    self.configure(case, [self.category])
                operation.assert_called_once()
                self.assertIs(operation.call_args.kwargs["model"], case[0])
                self.assertIs(operation.call_args.kwargs["mapping_model"], case[1])
                self.assertTrue(self.applies(case))

    def test_global_restricted_multiple_and_nowhere_each_type(self):
        for case in CASES:
            with self.subTest(model=case[0].__name__):
                self.assertFalse(self.applies(case))
                self.configure(case, global_mode=True)
                self.assertTrue(self.applies(case, self.unmapped))
                self.configure(case, [self.category, self.other, self.category])
                self.assertTrue(self.applies(case))
                self.assertTrue(self.applies(case, self.other))
                self.assertFalse(self.applies(case, self.unmapped))
                self.assertEqual(case[1].objects.count(), 2)
                self.configure(case)
                self.assertFalse(self.applies(case))
                self.assertFalse(case[1].objects.exists())

    def test_mapping_constraints_protect_parents_and_reject_reparenting(self):
        for case in CASES:
            with self.subTest(model=case[0].__name__), transaction.atomic():
                self.configure(case, [self.category])
                mapping = case[1].objects.get()
                self.assertIsInstance(mapping.pk, uuid.UUID)
                self.assertIsNotNone(mapping.created_at)
                self.assertIsNotNone(mapping.updated_at)
                values = {case[2]: self.record(case), "product_category": self.category}
                with self.assertRaises(ValidationError):
                    case[1].objects.create(**values)
                with self.assertRaises(IntegrityError), transaction.atomic():
                    case[1].objects.bulk_create([case[1](**values)])
                for parent in [self.record(case), self.category]:
                    with self.assertRaises(ProtectedError):
                        parent.delete()
                mapping.product_category = self.other
                with self.assertRaises(ValidationError):
                    mapping.save(update_fields=["product_category"])
                transaction.set_rollback(True)

    def test_direct_model_paths_reject_global_mapping_contradiction(self):
        for case in CASES:
            self.configure(case, [self.category])
            obj = self.record(case)
            obj.applies_to_all_product_categories = True
            with self.assertRaises(ValidationError):
                obj.save(update_fields=["applies_to_all_product_categories"])
            self.configure(case, global_mode=True)
            with self.assertRaises(ValidationError):
                case[1].objects.create(**{case[2]: obj}, product_category=self.category)

    def test_invalid_updates_preserve_previous_configuration(self):
        for case in CASES:
            self.configure(case, [self.category])
            original = case[1].objects.get()
            for categories, mode in [([self.other], True), ([ProductCategory(code="UNSAVED", name="Unsaved")], False), ([], "False")]:
                with self.assertRaises(ValidationError):
                    self.configure(case, categories, mode)
                self.assertEqual(case[1].objects.get().pk, original.pk)
                self.assertTrue(self.applies(case))

    def test_failed_replacement_restores_global_and_restricted_states_each_type(self):
        for case in CASES:
            with self.subTest(model=case[0].__name__):
                self.configure(case, [self.category])
                original = case[1].objects.get()
                with patch.object(case[1], "save", side_effect=RuntimeError("simulated mapping failure")):
                    with self.assertRaises(RuntimeError):
                        self.configure(case, [self.other])
                self.assertEqual(case[1].objects.get().pk, original.pk)
                self.assertTrue(self.applies(case))
                self.configure(case, global_mode=True)
                with patch.object(case[1], "save", side_effect=RuntimeError("simulated mapping failure")):
                    with self.assertRaises(RuntimeError):
                        self.configure(case, [self.category])
                self.assertTrue(self.record(case).applies_to_all_product_categories)
                self.assertFalse(case[1].objects.exists())

    def test_lifecycle_and_category_reactivation_reuse_configuration_each_type(self):
        for case in CASES:
            with self.subTest(model=case[0].__name__), transaction.atomic():
                self.configure(case, [self.category])
                mapping = case[1].objects.get()
                getattr(services, case[7])(**{case[3]: self.record(case)})
                self.assertFalse(self.applies(case))
                getattr(services, case[8])(**{case[3]: self.record(case)})
                self.assertTrue(self.applies(case))
                catalog_services.deactivate_category(category=self.category)
                self.assertFalse(self.applies(case))
                self.configure(case, [self.category])
                self.assertEqual(case[1].objects.get().pk, mapping.pk)
                self.configure(case, global_mode=True)
                self.assertFalse(self.applies(case))
                self.configure(case, [self.category])
                catalog_services.reactivate_category(category=self.category)
                self.assertTrue(self.applies(case))
                transaction.set_rollback(True)

    def test_query_order_deduplication_filters_and_bounded_sql_each_type(self):
        for case in CASES:
            model = case[0]
            global_record = model.objects.create(code="A_GLOBAL", name="Global", applies_to_all_product_categories=True)
            model.objects.create(code="UNMAPPED", name="Unmapped")
            model.objects.create(code="INACTIVE", name="Inactive", is_active=False, applies_to_all_product_categories=True)
            self.configure(case, [self.category, self.other])
            with self.assertNumQueries(0):
                result = getattr(queries, case[5])(self.category)
            with self.assertNumQueries(1):
                self.assertEqual(list(result), [global_record, self.record_without_query(case)])
            with self.assertNumQueries(1):
                self.assertEqual(list(getattr(queries, case[5])(self.category).filter(code="A_GLOBAL")), [global_record])

    def record_without_query(self, case):
        # UUID comparison needs no query: cache the fixture record before assertion.
        return self.cached_records[case[0]]

    def setUp(self):
        self.cached_records = {case[0]: self.record(case) for case in CASES}

    def test_queries_read_stored_state_and_deny_unsupported_inputs(self):
        for case in CASES:
            obj = self.record(case)
            self.configure(case, global_mode=True)
            getattr(services, case[7])(**{case[3]: obj})
            self.assertFalse(getattr(queries, case[6])(**{case[3]: obj}, product_category=self.category))
            self.assertFalse(getattr(queries, case[5])(ProductCategory(code="UNSAVED", name="Unsaved")).exists())
            self.assertFalse(getattr(queries, case[6])(**{case[3]: None}, product_category=self.category))

    def test_corrupt_global_plus_mapping_fails_closed_each_type(self):
        for case in CASES:
            self.configure(case, [self.category])
            case[0].objects.filter(pk=self.record(case).pk).update(applies_to_all_product_categories=True)
            self.assertFalse(self.applies(case))
            self.assertFalse(self.applies(case, self.unmapped))


class TechnicalAdminTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.category = ProductCategory.objects.create(code="A", name="A")
        cls.admin = get_user_model().objects.create_superuser(username="technical-admin", password="test-only-password")

    def setUp(self):
        self.client.force_login(self.admin)

    def create(self, case):
        with patch.object(services, case[4], wraps=getattr(services, case[4])) as operation:
            response = self.client.post(reverse(f"admin:service_catalog_{case[0]._meta.model_name}_add"), {
                "code": "NEW", "name": "New", "is_active": "on", "product_categories": [str(self.category.pk)],
            })
        self.assertEqual(response.status_code, 302)
        operation.assert_called_once()
        return case[0].objects.get(code="NEW")

    def test_all_admin_paths_create_and_transition_through_public_service(self):
        for case in CASES:
            obj = self.create(case)
            url = reverse(f"admin:service_catalog_{obj._meta.model_name}_change", args=[obj.pk])
            with patch.object(services, case[4], wraps=getattr(services, case[4])) as operation:
                response = self.client.post(url, {"code": obj.code, "name": obj.name, "global_applicability": "on"})
            self.assertEqual(response.status_code, 302)
            operation.assert_called_once()
            obj.refresh_from_db()
            self.assertTrue(obj.applies_to_all_product_categories)
            self.assertFalse(case[1].objects.exists())
            self.assertEqual(self.client.get(reverse(f"admin:service_catalog_{obj._meta.model_name}_changelist")).status_code, 200)
            self.assertEqual(Permission.objects.filter(content_type__app_label="service_catalog", content_type__model=obj._meta.model_name).count(), 4)
            self.assertFalse(is_authorized(user=self.admin, permission=f"service_catalog.view_{obj._meta.model_name}", target=obj))

    def test_admin_invalid_configuration_preserves_mappings_each_type(self):
        for case in CASES:
            obj = self.create(case)
            response = self.client.post(reverse(f"admin:service_catalog_{obj._meta.model_name}_change", args=[obj.pk]), {
                "code": obj.code, "name": obj.name, "global_applicability": "on", "product_categories": [str(self.category.pk)],
            })
            self.assertContains(response, "Global records cannot have explicit category mappings")
            self.assertEqual(case[1].objects.count(), 1)
            obj.refresh_from_db()
            self.assertFalse(obj.applies_to_all_product_categories)

    def test_admin_lifecycle_each_type_calls_explicit_service(self):
        for case in CASES:
            obj = self.create(case)
            url = reverse(f"admin:service_catalog_{obj._meta.model_name}_changelist")
            for action, service_name, expected in [("deactivate_selected", case[7], False), ("reactivate_selected", case[8], True)]:
                with patch.object(services, service_name, wraps=getattr(services, service_name)) as operation:
                    response = self.client.post(url, {"action": action, "_selected_action": [str(obj.pk)]})
                self.assertEqual(response.status_code, 302)
                operation.assert_called_once()
                obj.refresh_from_db()
                self.assertEqual(obj.is_active, expected)
                self.assertEqual(case[1].objects.count(), 1)

    def test_representative_admin_failure_rolls_back_master_edit(self):
        case = CASES[0]
        obj = self.create(case)
        with patch.object(services, case[4], side_effect=RuntimeError("simulated applicability failure")):
            with self.assertRaises(RuntimeError):
                self.client.post(reverse("admin:service_catalog_faultdiagnosis_change", args=[obj.pk]), {
                    "code": obj.code, "name": "Must roll back", "global_applicability": "on",
                })
        obj.refresh_from_db()
        self.assertEqual(obj.name, "New")
        self.assertEqual(case[1].objects.count(), 1)


class TechnicalConcurrencyTests(TransactionTestCase):
    run_concurrent = concurrency.AssignmentConcurrencyTests.run_concurrent

    def setUp(self):
        self.a = ProductCategory.objects.create(code="A", name="A")
        self.b = ProductCategory.objects.create(code="B", name="B")
        self.c = ProductCategory.objects.create(code="C", name="C")
        self.diagnosis = models.FaultDiagnosis.objects.create(code="A", name="A")
        self.root_cause = models.RootCause.objects.create(code="A", name="A")
        self.repair_action = models.RepairAction.objects.create(code="A", name="A")

    def test_diagnosis_global_then_restricted_serializes(self):
        self.run_concurrent(
            lambda: services.set_fault_diagnosis_applicability(diagnosis=self.diagnosis, applies_to_all_product_categories=True, product_categories=[]),
            lambda: services.set_fault_diagnosis_applicability(diagnosis=self.diagnosis, applies_to_all_product_categories=False, product_categories=[self.a, self.b]),
            expected="success")
        self.diagnosis.refresh_from_db()
        self.assertFalse(self.diagnosis.applies_to_all_product_categories)
        self.assertEqual(set(self.diagnosis.category_mappings.values_list("product_category_id", flat=True)), {self.a.pk, self.b.pk})

    def test_root_cause_restricted_then_global_serializes(self):
        self.run_concurrent(
            lambda: services.set_root_cause_applicability(root_cause=self.root_cause, applies_to_all_product_categories=False, product_categories=[self.a]),
            lambda: services.set_root_cause_applicability(root_cause=self.root_cause, applies_to_all_product_categories=True, product_categories=[]),
            expected="success")
        self.root_cause.refresh_from_db()
        self.assertTrue(self.root_cause.applies_to_all_product_categories)
        self.assertFalse(self.root_cause.category_mappings.exists())

    def test_repair_action_competing_replacements_are_complete(self):
        self.run_concurrent(
            lambda: services.set_repair_action_applicability(repair_action=self.repair_action, applies_to_all_product_categories=False, product_categories=[self.a, self.b]),
            lambda: services.set_repair_action_applicability(repair_action=self.repair_action, applies_to_all_product_categories=False, product_categories=[self.b, self.c]),
            expected="success")
        self.assertEqual(set(self.repair_action.category_mappings.values_list("product_category_id", flat=True)), {self.b.pk, self.c.pk})

    def test_global_diagnosis_blocks_then_rejects_direct_mapping(self):
        self.run_concurrent(
            lambda: services.set_fault_diagnosis_applicability(diagnosis=self.diagnosis, applies_to_all_product_categories=True, product_categories=[]),
            lambda: models.FaultDiagnosisProductCategory.objects.create(fault_diagnosis=self.diagnosis, product_category=self.a), expected="validation")
        self.assertFalse(models.FaultDiagnosisProductCategory.objects.exists())

    def test_category_deactivation_race_retains_unselectable_configuration(self):
        self.run_concurrent(lambda: catalog_services.deactivate_category(category=self.a),
            lambda: services.set_root_cause_applicability(root_cause=self.root_cause, applies_to_all_product_categories=False, product_categories=[self.a]),
            expected="success")
        self.assertEqual(self.root_cause.category_mappings.get().product_category, self.a)
        self.assertFalse(queries.root_cause_applies_to_category(root_cause=self.root_cause, product_category=self.a))
        catalog_services.reactivate_category(category=self.a)
        self.assertTrue(queries.root_cause_applies_to_category(root_cause=self.root_cause, product_category=self.a))
