<?php
/*
 * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * See the LICENSE file in the project root for the full license text.
 */

declare(strict_types=1);

namespace App\Tests\Services\Import;

use App\Domain\Import\ParsedAssignment;
use App\Domain\Import\ParsedSheet;
use App\Domain\Import\ParsedTask;
use App\Domain\Import\ParsedWeek;
use App\Domain\Import\ParsedWorker;
use App\Domain\TaskAssignment;
use App\Http\Request;
use App\Repositories\SprintRepository;
use App\Repositories\SprintWeekRepository;
use App\Repositories\SprintWorkerDayRepository;
use App\Repositories\SprintWorkerRepository;
use App\Repositories\TaskAssignmentRepository;
use App\Repositories\TaskRepository;
use App\Repositories\WorkerRepository;
use App\Services\AuditLogger;
use App\Services\Import\SprintImporter;
use App\Tests\TestCase;
use PDO;
use RuntimeException;

/**
 * Phase 20 — commit-side test. Builds a ParsedSheet by hand (no XLSX, no
 * PhpSpreadsheet) and asserts that committing it produces the expected
 * sprint, weeks, workers, sprint_workers, sprint_worker_days, tasks,
 * task_assignments, and audit rows.
 */
final class SprintImporterCommitTest extends TestCase
{
    /**
     * @return array{0:PDO,1:SprintImporter,2:Request}
     */
    private function build(): array
    {
        $pdo = $this->makeDb();
        $importer = new SprintImporter(
            pdo:          $pdo,
            sprints:      new SprintRepository($pdo),
            weeks:        new SprintWeekRepository($pdo),
            sprintWorkers:new SprintWorkerRepository($pdo),
            days:         new SprintWorkerDayRepository($pdo),
            tasks:        new TaskRepository($pdo),
            assignments:  new TaskAssignmentRepository($pdo),
            workers:      new WorkerRepository($pdo),
            audit:        new AuditLogger($pdo),
        );
        $req = new Request('POST', '/sprints/import/x', [], [], '', [], []);
        return [$pdo, $importer, $req];
    }

    private function sampleSheet(): ParsedSheet
    {
        return new ParsedSheet(
            sheetName: 'Sprint 1',
            weeks: [
                new ParsedWeek(1, 13, '23.03', 2, '2026-03-23'),
                new ParsedWeek(2, 14, '30.03', 4, '2026-03-30'),
            ],
            workers: [
                new ParsedWorker('Alice',    [1 => 2.0, 2 => 4.0], 0.5),
                new ParsedWorker('Bob',      [1 => 1.0, 2 => 0.0], 0.7),
            ],
            tasks: [
                new ParsedTask('Build thing',  'Alice', 1, [
                    new ParsedAssignment('Alice', 2.0, TaskAssignment::STATUS_ABGESCHLOSSEN, 'FF00B050'),
                    new ParsedAssignment('Bob',   1.0, TaskAssignment::STATUS_GESTARTET,    'FFFFFF00'),
                ]),
                new ParsedTask('Review thing', 'Carol', 2, [
                    new ParsedAssignment('Alice', 0.5, TaskAssignment::STATUS_ZUGEWIESEN,   null),
                ]),
            ],
            reserveFraction:   0.2,
            inferredStartDate: '2026-03-23',
            inferredEndDate:   '2026-04-05',
            warnings:          [],
        );
    }

    public function testCreateNewSprintWritesEverythingTransactionally(): void
    {
        [$pdo, $importer, $req] = $this->build();
        $sheet = $this->sampleSheet();

        $result = $importer->commit(
            sheet:            $sheet,
            sprintName:       'Sprint 1',
            startDate:        '2026-03-23',
            endDate:          '2026-04-05',
            target:           'new',
            existingSprintId: null,
            req:              $req,
            actor:            null,
        );

        $this->assertSame('Sprint 1', $result->sprintName);
        $this->assertSame(2, $result->weekCount);
        $this->assertSame(2, $result->workerCount);
        $this->assertSame(2, $result->taskCount);

        // The two parsed workers were both auto-created.
        $this->assertSame(['Alice', 'Bob'], $result->createdWorkers);
        // Carol (the second task's owner) doesn't exist as a worker — recorded as missing.
        $this->assertSame(['Carol'], $result->missingOwners);

        // Sprint row + reserve.
        $sprintRow = $pdo->query('SELECT * FROM sprints')->fetch();
        $this->assertSame('Sprint 1', $sprintRow['name']);
        $this->assertSame('2026-03-23', $sprintRow['start_date']);
        $this->assertEqualsWithDelta(0.2, (float) $sprintRow['reserve_fraction'], 1e-9);

        // 2 weeks with active_days_mask matching maxDays.
        $weeks = $pdo->query('SELECT * FROM sprint_weeks ORDER BY sort_order')->fetchAll();
        $this->assertCount(2, $weeks);
        $this->assertSame(0b00011, (int) $weeks[0]['active_days_mask'], '2 days → Mo+Di');
        $this->assertSame(0b01111, (int) $weeks[1]['active_days_mask'], '4 days → Mo..Do');
        $this->assertEqualsWithDelta(2.0, (float) $weeks[0]['max_working_days'], 1e-9);
        $this->assertEqualsWithDelta(4.0, (float) $weeks[1]['max_working_days'], 1e-9);

        // 2 sprint_workers with RTBs.
        $sw = $pdo->query('SELECT sw.*, w.name FROM sprint_workers sw JOIN workers w ON w.id=sw.worker_id ORDER BY sw.sort_order')->fetchAll();
        $this->assertCount(2, $sw);
        $this->assertSame('Alice', $sw[0]['name']);
        $this->assertEqualsWithDelta(0.5, (float) $sw[0]['rtb'], 1e-9);
        $this->assertSame('Bob',   $sw[1]['name']);
        $this->assertEqualsWithDelta(0.7, (float) $sw[1]['rtb'], 1e-9);

        // sprint_worker_days: 3 non-zero cells (Alice w1+w2, Bob w1; Bob w2 was 0).
        $dayCells = (int) $pdo->query('SELECT COUNT(*) FROM sprint_worker_days')->fetchColumn();
        $this->assertSame(3, $dayCells);

        // 2 tasks; first owned by Alice, second has no owner (Carol unknown).
        $tasks = $pdo->query('SELECT * FROM tasks ORDER BY sort_order')->fetchAll();
        $this->assertCount(2, $tasks);
        $this->assertSame('Build thing',  $tasks[0]['title']);
        $this->assertNotNull($tasks[0]['owner_worker_id']);
        $this->assertSame('Review thing', $tasks[1]['title']);
        $this->assertNull($tasks[1]['owner_worker_id']);
        $this->assertSame(1, (int) $tasks[0]['priority']);
        $this->assertSame(2, (int) $tasks[1]['priority']);

        // Task assignments: 2 days+colour for task 1, none for task 2 (the
        // single ZUGEWIESEN cell with 0.5 days writes a row but no status row).
        $aRows = $pdo->query('SELECT ta.*, t.title FROM task_assignments ta JOIN tasks t ON t.id=ta.task_id ORDER BY t.sort_order, ta.sprint_worker_id')->fetchAll();
        $this->assertCount(3, $aRows, 'three assignment rows total');

        // Status: the green cell → abgeschlossen, the yellow cell → gestartet,
        // the default zugewiesen cell stays at default.
        $statuses = array_map(fn($r) => $r['status'], $aRows);
        sort($statuses);
        $this->assertSame(['abgeschlossen', 'gestartet', 'zugewiesen'], $statuses);

        // Audit log: sprint CREATE + IMPORTED_FROM_XLSX + 2× sprint_week CREATE
        // + 2× worker CREATE + 2× sprint_worker CREATE + 3× sprint_worker_day CREATE
        // + 2× task CREATE + 3× task_assignment CREATE + 2× task_assignment status UPDATE/CREATE.
        $auditCount = (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn();
        $this->assertGreaterThan(15, $auditCount, 'every write is audited');

        $importedRows = (int) $pdo->query("SELECT COUNT(*) FROM audit_log WHERE action='IMPORTED_FROM_XLSX'")->fetchColumn();
        $this->assertSame(1, $importedRows);
    }

    public function testRefusesToImportIntoNonEmptyExistingSprint(): void
    {
        [$pdo, $importer, $req] = $this->build();
        $sprints = new SprintRepository($pdo);
        $sprint = $sprints->create('Existing', '2026-03-23', '2026-04-05', 0.2);
        $sprints->materializeWeeks($sprint->id, '2026-03-23', '2026-04-05', 2); // populates sprint_weeks

        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('not empty');

        $importer->commit(
            sheet:            $this->sampleSheet(),
            sprintName:       'Existing',
            startDate:        '2026-03-23',
            endDate:          '2026-04-05',
            target:           'existing',
            existingSprintId: $sprint->id,
            req:              $req,
            actor:            null,
        );
    }

    public function testReusesExistingWorkerByCaseFoldedName(): void
    {
        [$pdo, $importer, $req] = $this->build();
        // Pre-create Alice with non-matching case + extra spaces.
        (new WorkerRepository($pdo))->create('  alice  ', true, 0.0);

        $result = $importer->commit(
            sheet:            $this->sampleSheet(),
            sprintName:       'S',
            startDate:        '2026-03-23',
            endDate:          '2026-04-05',
            target:           'new',
            existingSprintId: null,
            req:              $req,
            actor:            null,
        );

        // Bob is created; Alice already existed (matched by case-folded name).
        $this->assertSame(['Bob'], $result->createdWorkers);
        $this->assertSame(2, (int) $pdo->query('SELECT COUNT(*) FROM workers')->fetchColumn());
    }

    public function testMaxDaysToMaskSemantics(): void
    {
        $this->assertSame(0,       SprintImporter::maxDaysToMask(0));
        $this->assertSame(0b00001, SprintImporter::maxDaysToMask(1));
        $this->assertSame(0b00011, SprintImporter::maxDaysToMask(2));
        $this->assertSame(0b00111, SprintImporter::maxDaysToMask(3));
        $this->assertSame(0b01111, SprintImporter::maxDaysToMask(4));
        $this->assertSame(0b11111, SprintImporter::maxDaysToMask(5));
        $this->assertSame(0b11111, SprintImporter::maxDaysToMask(7), 'caps at 5');
        $this->assertSame(0,       SprintImporter::maxDaysToMask(-1));
    }

    public function testFoldNormalisation(): void
    {
        $this->assertSame('alice', SprintImporter::fold('Alice'));
        $this->assertSame('alice', SprintImporter::fold('  ALICE  '));
        $this->assertSame('michael br', SprintImporter::fold('Michael  Br'));
        $this->assertSame('jürg', SprintImporter::fold('Jürg'));
    }
}
