"""Small, read-only project check command using Django's existing test runner."""
from contextlib import contextmanager
from pathlib import PurePosixPath
import os
import subprocess
import time
from uuid import uuid4

from django.conf import settings
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from django.db import connections
from django.db.migrations.executor import MigrationExecutor


SMOKE_TESTS = (
    "tests.test_foundation",
    "apps.access.test_security_audit.AuthenticationSecurityTests",
    "apps.access.test_security_audit.FinalAuthorizationAuditTests",
)


@contextmanager
def isolated_test_database():
    # This project has one PostgreSQL database. Fail safely if that changes.
    if list(connections) != ["default"]:
        raise CommandError("Audit test isolation requires review for multiple databases.")
    connection = connections["default"]
    config = connection.settings_dict
    options = config["TEST"]
    if (config["ENGINE"] != "django.db.backends.postgresql"
            or options.get("MIRROR") or options.get("MIGRATE") is False):
        raise CommandError("Audit requires PostgreSQL with test migrations and no mirror.")
    original_name, original_test_name = config["NAME"], options.get("NAME")
    # Avoid another test process's shared database. Django owns its lifecycle.
    candidate = "audit_" + uuid4().hex
    while candidate in (original_name, original_test_name):
        candidate = "audit_" + uuid4().hex
    options["NAME"] = candidate
    try:
        yield
    finally:
        if config["NAME"] != original_name:
            connection.close()
            config["NAME"] = original_name
        options["NAME"] = original_test_name


class Command(BaseCommand):
    help = "Run project checks and security smoke tests; --full runs the entire suite."
    requires_system_checks = []

    def add_arguments(self, parser):
        modes = parser.add_mutually_exclusive_group()
        modes.add_argument("--quick", action="store_true", help="Checks and small existing smoke suite (default).")
        modes.add_argument("--full", action="store_true", help="Checks and complete Django regression; may take an hour.")

    def git(self, *arguments):
        return subprocess.run(
            ["git", "-c", "safe.directory=" + str(settings.BASE_DIR),
             "-c", "core.fsmonitor=false", "--no-pager", *arguments],
            cwd=settings.BASE_DIR, env=dict(os.environ, GIT_OPTIONAL_LOCKS="0"),
            shell=False, check=True, capture_output=True, text=True,
            encoding="utf-8", errors="replace", timeout=30,
        ).stdout

    def check_migrations(self):
        executor = MigrationExecutor(connections["default"])
        executor.loader.check_consistent_history(connections["default"])
        if (executor.loader.detect_conflicts()
                or executor.migration_plan(executor.loader.graph.leaf_nodes())):
            raise CommandError("Unapplied or conflicting migrations.")

    def check_env_tracking(self):
        tracked = self.git("ls-files", "-z").split("\0")
        if any(PurePosixPath(path).name.lower() == ".env" for path in tracked):
            raise CommandError("An .env file is tracked by Git.")

    def run_tests(self, full):
        previous = os.getcwd()
        try:
            os.chdir(settings.BASE_DIR)
            with isolated_test_database():
                # Empty labels mean complete discovery, including smoke tests.
                call_command("test", *(() if full else SMOKE_TESTS),
                             interactive=False, keepdb=False,
                             stdout=self.stdout, stderr=self.stderr)
        finally:
            os.chdir(previous)

    def handle(self, *args, **options):
        if options["quick"] and options["full"]:
            raise CommandError("Choose --quick or --full, not both.")
        started = time.monotonic()
        failed = []
        self.stdout.write("C-CARE AUDIT: " + ("FULL" if options["full"] else "QUICK"))
        checks = (
            ("Django system check", lambda: call_command("check", stdout=self.stdout, stderr=self.stderr)),
            ("Migration drift", lambda: call_command("makemigrations", check=True, dry_run=True,
                                                     interactive=False, stdout=self.stdout, stderr=self.stderr)),
            ("Unapplied migrations", self.check_migrations),
            ("Git whitespace", lambda: self.git("diff", "--no-ext-diff", "--no-textconv", "--check")),
            (".env not tracked", self.check_env_tracking),
            ("Full regression" if options["full"] else "Security/integrity smoke tests",
             lambda: self.run_tests(options["full"])),
        )
        for name, operation in checks:
            try:
                operation()
            except (Exception, SystemExit) as error:
                failed.append(name)
                # Do not echo raw exception/subprocess values or file contents.
                self.stderr.write(f"FAIL: {name} ({type(error).__name__})")
            else:
                self.stdout.write("PASS: " + name)
        self.stdout.write(f"Elapsed: {time.monotonic() - started:.3f}s")
        if failed:
            raise CommandError("Audit failed: " + ", ".join(failed))
        self.stdout.write("AUDIT CHECKS PASSED")
