<?php

namespace MintyPHP\Tests\Architecture;

final class ModuleUiContractTest extends AbstractModuleStructureContractTestCase
{
    public function testUiSlotAbilitiesMatchPolicySupports(): void
    {
        foreach (self::$modules as $module) {
            $supportedAbilities = [];
            foreach ($module['manifest']->authorizationPolicies as $policyClass) {
                if (!class_exists($policyClass)) {
                    continue;
                }
                $reflection = new \ReflectionClass($policyClass);
                foreach ($reflection->getConstants() as $constValue) {
                    if (is_string($constValue)) {
                        $supportedAbilities[] = $constValue;
                    }
                }
            }

            if ($supportedAbilities === []) {
                continue;
            }

            foreach ($module['manifest']->uiSlots as $slotName => $contributions) {
                $contributionList = is_array($contributions) ? $contributions : [$contributions];
                foreach ($contributionList as $idx => $contribution) {
                    if (!is_array($contribution)) {
                        continue;
                    }
                    $ability = trim((string) ($contribution['permission'] ?? ''));
                    if ($ability === '') {
                        continue;
                    }
                    self::assertContains(
                        $ability,
                        $supportedAbilities,
                        "Module '{$module['id']}' ui_slots['{$slotName}'][{$idx}] permission '{$ability}' "
                        . "is not declared as a constant on any module authorization policy"
                    );
                }
            }
        }
    }

    public function testPermissionsUseObjectContract(): void
    {
        foreach (self::$modules as $module) {
            foreach ($module['manifest']->permissions as $index => $permission) {
                self::assertArrayHasKey('key', $permission, "Module '{$module['id']}' permissions[{$index}] missing key");
                self::assertArrayHasKey('description', $permission, "Module '{$module['id']}' permissions[{$index}] missing description");
                self::assertNotEmpty(trim((string) $permission['key']), "Module '{$module['id']}' permissions[{$index}] key must be non-empty");
                self::assertNotEmpty(trim((string) $permission['description']), "Module '{$module['id']}' permissions[{$index}] description must be non-empty");
                self::assertContains((int) $permission['active'], [0, 1], "Module '{$module['id']}' permissions[{$index}] active must be 0|1");
                self::assertContains((int) $permission['is_system'], [0, 1], "Module '{$module['id']}' permissions[{$index}] is_system must be 0|1");
            }
        }
    }

    public function testSchedulerJobsUseFullContractSchema(): void
    {
        foreach (self::$modules as $module) {
            foreach ($module['manifest']->schedulerJobs as $index => $job) {
                $required = [
                    'job_key',
                    'handler',
                    'label',
                    'description',
                    'default_enabled',
                    'default_timezone',
                    'default_schedule_type',
                    'default_schedule_interval',
                    'default_schedule_time',
                    'default_schedule_weekdays_csv',
                    'default_catchup_once',
                    'allowed_schedule_types',
                ];

                foreach ($required as $requiredKey) {
                    self::assertArrayHasKey(
                        $requiredKey,
                        $job,
                        "Module '{$module['id']}' scheduler_jobs[{$index}] missing '{$requiredKey}'"
                    );
                }

                self::assertNotEmpty(trim((string) $job['job_key']), "Module '{$module['id']}' scheduler_jobs[{$index}] job_key must be non-empty");
                self::assertNotEmpty(trim((string) $job['handler']), "Module '{$module['id']}' scheduler_jobs[{$index}] handler must be non-empty");
                self::assertNotEmpty(trim((string) $job['label']), "Module '{$module['id']}' scheduler_jobs[{$index}] label must be non-empty");
                self::assertContains(
                    (string) $job['default_schedule_type'],
                    ['hourly', 'daily', 'weekly'],
                    "Module '{$module['id']}' scheduler_jobs[{$index}] default_schedule_type invalid"
                );

                $allowedTypes = $job['allowed_schedule_types'];
                self::assertNotEmpty($allowedTypes, "Module '{$module['id']}' scheduler_jobs[{$index}] allowed_schedule_types must be non-empty");
                foreach ($allowedTypes as $allowedType) {
                    self::assertContains(
                        (string) $allowedType,
                        ['hourly', 'daily', 'weekly'],
                        "Module '{$module['id']}' scheduler_jobs[{$index}] allowed_schedule_types entry invalid"
                    );
                }
            }
        }

        self::addToAssertionCount(1);
    }

    public function testUiSlotContributionsHaveRequiredKeys(): void
    {
        $slotKeyRequirements = [
            'aside.tab_panel' => ['key', 'label', 'icon', 'permission', 'panel_template'],
            'search.resource_item' => ['key', 'label', 'base_url', 'permission'],
            'user.edit.aside_action' => ['key', 'type', 'label', 'permission'],
            'topbar.right_item' => ['key', 'template'],
            'layout.body_end_template' => ['key', 'template'],
            'layout.head_style' => ['key', 'path'],
            'runtime.component' => ['key', 'script'],
        ];

        foreach (self::$modules as $module) {
            foreach ($module['manifest']->uiSlots as $slotName => $contributions) {
                if (!isset($slotKeyRequirements[$slotName])) {
                    continue;
                }

                $required = $slotKeyRequirements[$slotName];
                $contributionList = is_array($contributions) ? $contributions : [$contributions];

                foreach ($contributionList as $idx => $contribution) {
                    if (!is_array($contribution)) {
                        continue;
                    }
                    foreach ($required as $requiredKey) {
                        self::assertArrayHasKey(
                            $requiredKey,
                            $contribution,
                            "Module '{$module['id']}' ui_slots['{$slotName}'][{$idx}] is missing required key '{$requiredKey}'"
                        );
                    }
                }
            }
        }
    }

    public function testPanelTemplatesExist(): void
    {
        foreach (self::$modules as $module) {
            $templateSlots = [
                'aside.tab_panel' => 'panel_template',
                'topbar.right_item' => 'template',
                'layout.body_end_template' => 'template',
            ];

            foreach ($templateSlots as $slotName => $templateKey) {
                $contributions = $module['manifest']->uiSlots[$slotName] ?? [];
                if (!is_array($contributions)) {
                    continue;
                }
                foreach ($contributions as $contribution) {
                    if (!is_array($contribution)) {
                        continue;
                    }
                    $template = $contribution[$templateKey] ?? null;
                    if ($template === null) {
                        continue;
                    }
                    $templatePath = (string) $template;
                    if (!str_starts_with($templatePath, '/')) {
                        $templatePath = $module['manifest']->basePath . '/' . $templatePath;
                    }
                    self::assertFileExists(
                        $templatePath,
                        "Module '{$module['id']}' {$slotName} {$templateKey} does not exist: {$templatePath}"
                    );
                }
            }
        }
    }

    public function testRuntimeComponentPhasesAreValid(): void
    {
        foreach (self::$modules as $module) {
            $runtimeComponents = $module['manifest']->uiSlots['runtime.component'] ?? [];
            if (!is_array($runtimeComponents)) {
                continue;
            }

            foreach ($runtimeComponents as $index => $contribution) {
                if (!is_array($contribution)) {
                    continue;
                }

                $phase = strtolower(trim((string) ($contribution['phase'] ?? 'late')));
                self::assertContains(
                    $phase,
                    ['early', 'default', 'late'],
                    "Module '{$module['id']}' runtime.component[{$index}] has invalid phase '{$phase}'"
                );
            }
        }
    }

    public function testSessionProvidersUseModulePrefixedKeys(): void
    {
        foreach (self::$modules as $module) {
            foreach ($module['manifest']->sessionProviders as $providerClass) {
                if (!class_exists($providerClass)) {
                    continue;
                }

                $reflection = new \ReflectionClass($providerClass);
                $file = $reflection->getFileName();
                if ($file === false) {
                    continue;
                }

                $content = file_get_contents($file);
                if ($content === false) {
                    continue;
                }

                $expectedPrefix = 'module.' . $module['id'] . '.';

                if (preg_match_all('/\$_SESSION\[\'([^\']+)\'\]/', $content, $matches)) {
                    foreach ($matches[1] as $key) {
                        self::assertStringStartsWith(
                            $expectedPrefix,
                            $key,
                            "Module '{$module['id']}' session provider '{$providerClass}' uses session key "
                            . "'{$key}' which does not follow the '{$expectedPrefix}*' convention"
                        );
                    }
                }

                if (preg_match_all('/->set\(\s*\'([^\']+)\'/', $content, $matches)) {
                    foreach ($matches[1] as $key) {
                        self::assertStringStartsWith(
                            $expectedPrefix,
                            $key,
                            "Module '{$module['id']}' session provider '{$providerClass}' uses session store key "
                            . "'{$key}' which does not follow the '{$expectedPrefix}*' convention"
                        );
                    }
                }
            }
        }

        self::addToAssertionCount(1);
    }
}
