<?php

namespace MintyPHP\Tests\Module\Security\Service;

use MintyPHP\Module\Security\Repository\SecurityCheckRepository;
use MintyPHP\Module\Security\Service\SecurityCheckProcess;
use MintyPHP\Module\Security\Service\SecurityCheckService;
use MintyPHP\Module\Security\Service\SecurityCheckTemplateService;
use PHPUnit\Framework\TestCase;

class SecurityCheckServiceTest extends TestCase
{
    private const TENANT_ID = 1;

    /**
     * @return array{0: SecurityCheckService, 1: SecurityCheckRepository, 2: SecurityCheckTemplateService}
     */
    private function makeService(?SecurityCheckRepository $repo = null, ?SecurityCheckTemplateService $templates = null): array
    {
        $repo = $repo ?? $this->createMock(SecurityCheckRepository::class);
        $templates = $templates ?? $this->createMock(SecurityCheckTemplateService::class);
        $service = new SecurityCheckService($repo, $templates, new SecurityCheckProcess());

        return [$service, $repo, $templates];
    }

    public function testCreateRequiresCustomerDomainAndProduct(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->expects($this->never())->method('insert');
        [$service] = $this->makeService($repo);

        $result = $service->create(self::TENANT_ID, [], 5);

        $this->assertFalse($result['ok']);
        $this->assertArrayHasKey('debitor_no', $result['errors']);
        $this->assertArrayHasKey('domain_no', $result['errors']);
        $this->assertArrayHasKey('product_code', $result['errors']);
    }

    public function testCreateFailsWhenTemplateMissing(): void
    {
        $templates = $this->createMock(SecurityCheckTemplateService::class);
        $templates->method('findByCode')->willReturn(null);
        [$service] = $this->makeService(null, $templates);

        $result = $service->create(self::TENANT_ID, [
            'debitor_no' => 'D1',
            'domain_no' => 'DNS1',
            'product_code' => 'intranet',
        ], 5);

        $this->assertFalse($result['ok']);
        $this->assertArrayHasKey('product_code', $result['errors']);
    }

    public function testCreateSucceedsAndSnapshotsSchema(): void
    {
        $templates = $this->createMock(SecurityCheckTemplateService::class);
        $templates->method('findByCode')->willReturn(['id' => 7, 'product_name' => 'Intranet', 'active' => 1]);
        $templates->method('decodeSchema')->willReturn(['version' => 2, 'items' => [['type' => 'check', 'key' => 'tls', 'label' => 'TLS']]]);

        $captured = null;
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('insert')->willReturnCallback(function (int $tenantId, array $data) use (&$captured) {
            $captured = $data;

            return 42;
        });

        [$service] = $this->makeService($repo, $templates);

        $result = $service->create(self::TENANT_ID, [
            'debitor_no' => 'D1',
            'debitor_name' => 'Acme',
            'domain_no' => 'DNS1',
            'domain_url' => 'acme.de',
            'product_code' => 'intranet',
            'owner_user_id' => 9,
        ], 5);

        $this->assertTrue($result['ok']);
        $this->assertSame(42, $result['id']);
        $this->assertSame(SecurityCheckProcess::STATUS_OPEN, $captured['status']);
        $this->assertSame(7, $captured['template_id']);
        $this->assertSame('Intranet', $captured['product_name']);
        $snapshot = json_decode((string) $captured['tech_schema_snapshot_json'], true);
        $this->assertSame('tls', $snapshot['items'][0]['key']);
    }

    public function testSaveStateStampsCompletionAndDerivesCompletedStatus(): void
    {
        $snapshot = json_encode(['version' => 1, 'items' => [['type' => 'check', 'key' => 'tls', 'label' => 'TLS']]]);
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('findById')->willReturn([
            'id' => 1,
            'status' => SecurityCheckProcess::STATUS_OPEN,
            'process_state_json' => '{}',
            'tech_schema_snapshot_json' => $snapshot,
            'tech_state_json' => '{}',
        ]);

        $captured = null;
        $repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
            $captured = $data;

            return true;
        });

        $process = new SecurityCheckProcess();
        [$service] = $this->makeService($repo);

        $step = [];
        foreach ($process->manualStepKeys() as $key) {
            $step[$key] = ['done' => '1', 'note' => ''];
        }
        $step[SecurityCheckProcess::STEP_INFORMATION]['fields'] = [
            'technical_contact' => 'Jane',
            'deployment' => 'Cloud',
            'system_info' => 'PHP 8.5',
            'external_systems' => 'https://api.example.test',
        ];
        $step['scheduling']['fields'] = [
            'golive_start' => '2026-07-01T09:00',
            'golive_end' => '2026-07-01T12:00',
        ];
        $step['blocker_appointment']['fields'] = [
            'blocker_start' => '2026-07-01T08:00',
            'blocker_end' => '2026-07-02T18:00',
        ];
        $step[SecurityCheckProcess::STEP_REPORT]['fields'] = [
            SecurityCheckProcess::FIELD_CUSTOMER_REPORT => 'All checks passed; no action required.',
        ];
        $input = [
            'step' => $step,
            'tech' => ['tls' => ['done' => '1', 'note' => 'verified']],
        ];

        $result = $service->saveState(self::TENANT_ID, 1, $input, 77);

        $this->assertTrue($result['ok']);
        $this->assertSame(SecurityCheckProcess::STATUS_COMPLETED, $captured['status']);

        $process_state = json_decode((string) $captured['process_state_json'], true);
        $this->assertTrue($process_state['beauftragung']['done']);
        $this->assertSame(77, $process_state['beauftragung']['by']);
        $this->assertNotEmpty($process_state['beauftragung']['at']);
        $this->assertSame('Jane', $process_state[SecurityCheckProcess::STEP_INFORMATION]['fields']['technical_contact']);
        $this->assertSame(
            'All checks passed; no action required.',
            $process_state[SecurityCheckProcess::STEP_REPORT]['fields'][SecurityCheckProcess::FIELD_CUSTOMER_REPORT]
        );

        $tech_state = json_decode((string) $captured['tech_state_json'], true);
        $this->assertTrue($tech_state['tls']['done']);
        $this->assertSame('verified', $tech_state['tls']['note']);
    }

    public function testSaveStateGatesInformationStepUntilRequiredFieldsFilled(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('findById')->willReturn([
            'id' => 1,
            'status' => SecurityCheckProcess::STATUS_OPEN,
            'process_state_json' => '{}',
            'tech_schema_snapshot_json' => '{}',
            'tech_state_json' => '{}',
        ]);

        $captured = null;
        $repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
            $captured = $data;

            return true;
        });

        [$service] = $this->makeService($repo);

        $result = $service->saveState(self::TENANT_ID, 1, [
            'step' => [SecurityCheckProcess::STEP_INFORMATION => [
                'done' => '1',
                'note' => '',
                'fields' => ['technical_contact' => 'Jane'], // deployment / system_info / external_systems missing
            ]],
            'tech' => [],
        ], 77);

        // Saved (values persisted) but the step is held open + a warning is returned.
        $this->assertTrue($result['ok']);
        $this->assertArrayHasKey('warning', $result);

        $process_state = json_decode((string) $captured['process_state_json'], true);
        $this->assertFalse($process_state[SecurityCheckProcess::STEP_INFORMATION]['done']);
        $this->assertSame('Jane', $process_state[SecurityCheckProcess::STEP_INFORMATION]['fields']['technical_contact']);
        $this->assertNotSame(SecurityCheckProcess::STATUS_COMPLETED, $captured['status']);
    }

    public function testSaveStateCompletesInformationStepWhenAllRequiredFieldsFilled(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('findById')->willReturn([
            'id' => 1,
            'status' => SecurityCheckProcess::STATUS_OPEN,
            'process_state_json' => '{}',
            'tech_schema_snapshot_json' => '{}',
            'tech_state_json' => '{}',
        ]);

        $captured = null;
        $repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
            $captured = $data;

            return true;
        });

        [$service] = $this->makeService($repo);

        $result = $service->saveState(self::TENANT_ID, 1, [
            'step' => [SecurityCheckProcess::STEP_INFORMATION => [
                'done' => '1',
                'note' => '',
                'fields' => [
                    'technical_contact' => 'Jane',
                    'deployment' => 'Cloud',
                    'system_info' => 'PHP 8.5',
                    'external_systems' => 'https://api.example.test',
                    // special_notes intentionally left empty — it is optional
                ],
            ]],
            'tech' => [],
        ], 77);

        $this->assertTrue($result['ok']);
        $this->assertArrayNotHasKey('warning', $result);

        $process_state = json_decode((string) $captured['process_state_json'], true);
        $this->assertTrue($process_state[SecurityCheckProcess::STEP_INFORMATION]['done']);
        $this->assertSame(77, $process_state[SecurityCheckProcess::STEP_INFORMATION]['by']);
    }

    public function testSaveStateGatesDatetimeStepUntilBothFieldsFilled(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('findById')->willReturn([
            'id' => 1,
            'status' => SecurityCheckProcess::STATUS_OPEN,
            'process_state_json' => '{}',
            'tech_schema_snapshot_json' => '{}',
            'tech_state_json' => '{}',
        ]);

        $captured = null;
        $repo->method('updateState')->willReturnCallback(function (int $tenantId, int $id, array $data) use (&$captured) {
            $captured = $data;

            return true;
        });

        [$service] = $this->makeService($repo);

        // Scheduling needs both datetimes; with only one filled it stays open.
        $result = $service->saveState(self::TENANT_ID, 1, [
            'step' => ['scheduling' => [
                'done' => '1',
                'note' => '',
                'fields' => ['golive_start' => '2026-07-01T09:00'], // golive_end missing
            ]],
            'tech' => [],
        ], 77);

        $this->assertTrue($result['ok']);
        $this->assertArrayHasKey('warning', $result);

        $process_state = json_decode((string) $captured['process_state_json'], true);
        $this->assertFalse($process_state['scheduling']['done']);
        $this->assertSame('2026-07-01T09:00', $process_state['scheduling']['fields']['golive_start']);
    }

    public function testSaveStateRejectsArchivedCheck(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('findById')->willReturn([
            'id' => 1,
            'status' => SecurityCheckProcess::STATUS_ARCHIVED,
            'process_state_json' => '{}',
            'tech_schema_snapshot_json' => '{}',
            'tech_state_json' => '{}',
        ]);
        $repo->expects($this->never())->method('updateState');

        [$service] = $this->makeService($repo);

        $result = $service->saveState(self::TENANT_ID, 1, ['step' => [], 'tech' => []], 5);

        $this->assertFalse($result['ok']);
        $this->assertArrayHasKey('general', $result['errors']);
    }

    public function testSetArchivedUpdatesStatusToArchived(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('findById')->willReturn([
            'id' => 1,
            'status' => SecurityCheckProcess::STATUS_IN_PROGRESS,
            'process_state_json' => '{}',
            'tech_schema_snapshot_json' => '{}',
            'tech_state_json' => '{}',
        ]);

        $capturedStatus = null;
        $repo->method('updateStatus')->willReturnCallback(function (int $tenantId, int $id, string $status) use (&$capturedStatus) {
            $capturedStatus = $status;

            return true;
        });

        [$service] = $this->makeService($repo);

        $result = $service->setArchived(self::TENANT_ID, 1, true, 5);

        $this->assertTrue($result['ok']);
        $this->assertSame(SecurityCheckProcess::STATUS_ARCHIVED, $capturedStatus);
    }

    public function testListPagedDelegatesToRepository(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('listPaged')->willReturn(['total' => 3, 'rows' => [['id' => 1]]]);

        [$service] = $this->makeService($repo);
        $result = $service->listPaged(self::TENANT_ID, ['search' => 'x']);

        $this->assertSame(3, $result['total']);
        $this->assertCount(1, $result['rows']);
    }

    public function testListAssignableUsersDelegatesToRepository(): void
    {
        $repo = $this->createMock(SecurityCheckRepository::class);
        $repo->method('listTenantUsers')->willReturn([
            ['id' => 3, 'display_name' => 'Alice'],
            ['id' => 7, 'display_name' => 'Bob'],
        ]);

        [$service] = $this->makeService($repo);
        $users = $service->listAssignableUsers(self::TENANT_ID);

        $this->assertCount(2, $users);
        $this->assertSame('Alice', $users[0]['display_name']);
    }
}
