<?php

app_require('classes/department_outside_hours_statistics_service.php');
app_require('routes/departmentDailyReportsRoute.php');

use classes\department_outside_hours_statistics_service;
use routes\departmentDailyReportsRoute;

function department_daily_reports_route_invoke_private(object $route, string $method, array $args = []): mixed
{
    $reflection = new ReflectionClass(departmentDailyReportsRoute::class);
    $target = $reflection->getMethod($method);

    return $target->invokeArgs($route, $args);
}

final class FakeDailyReportValue
{
    public function __construct(private readonly mixed $current)
    {
    }

    public function value(): mixed
    {
        return $this->current;
    }
}

final class FakeDailyReportVariables
{
    public function __construct(private readonly array $values = [])
    {
    }

    public function getVariable(string $key): mixed
    {
        return $this->values[$key] ?? null;
    }
}

final class FakeDailyReportRepository
{
    public array $transaction_summary = [
        'quantity' => 0,
        'products' => 0,
        'earnings' => 0,
        'washes' => 0,
        'water_usage' => 0,
    ];

    public array $booking_summary = [
        'completed' => 0,
        'total' => 0,
    ];

    public array $product_overview = [];

    public array $wash_transactions = [];

    public function getTransactionSummaryForDepartments(string $date, array $department_ids, string $date_to = null): array
    {
        return $this->transaction_summary;
    }

    public function getBookingSummaryForDepartments(string $date, array $department_ids, string $date_to = null): array
    {
        return $this->booking_summary;
    }

    public function getProductOverviewForDepartments(string $date, array $department_ids, array $product_ids, string $date_to = null): array
    {
        return $this->product_overview;
    }

    public function getWashTransactionsForDepartments(string $date, array $department_ids, string $date_to = null): array
    {
        return $this->wash_transactions;
    }
}

final class FakeDailyReportComplaintsRepository
{
    public int $count = 0;

    public function countForDepartmentsInRange(array $department_ids, string $date, ?string $date_to = null): int
    {
        return $this->count;
    }
}

final class FakeOutsideHoursStatisticsService extends department_outside_hours_statistics_service
{
    public array $summary = [
        'total' => 0,
        'by_source' => [
            'orders' => 0,
            'xlvask' => 0,
            'selfserve' => 0,
        ],
        'has_missing_opening_hours' => false,
        'missing_department_ids' => [],
    ];

    public function __construct()
    {
    }

    public function getSummary(string $date, array|int|string $department_ids, ?string $date_to = null): array
    {
        return [
            'department_ids' => is_array($department_ids) ? array_values(array_map('intval', $department_ids)) : [(int)$department_ids],
            'date' => $date,
            'date_to' => $date_to ?? $date,
            ...$this->summary,
        ];
    }
}

final class DepartmentDailyReportsOverviewRouteDouble extends departmentDailyReportsRoute
{
    public object $repository;
    public object $complaints_repository;
    public array $opening_hours = [];
    public array $departments = [];
    public array $product_targets_by_department = [];
    public array $workfeed_departments = [];
    public array $workfeed_shifts = [];
    public department_outside_hours_statistics_service $outside_hours_service;

    protected function dailyReportRepository(): object
    {
        return $this->repository;
    }

    protected function dailyReportComplaintsRepository(): object
    {
        return $this->complaints_repository;
    }

    protected function fetchOpeningHoursByDepartmentId(array $department_ids): array
    {
        return $this->opening_hours;
    }

    protected function fetchDepartmentsByIds(array $department_ids): array
    {
        return $this->departments;
    }

    protected function fetchWorkfeedDepartments(): array
    {
        return $this->workfeed_departments;
    }

    protected function fetchWorkfeedShifts(\DateTime $query_start, \DateTime $range_end_exclusive): array
    {
        return $this->workfeed_shifts;
    }

    protected function outsideHoursStatisticsService(): department_outside_hours_statistics_service
    {
        return $this->outside_hours_service;
    }

    protected function getDailyReportProductTargetsForDepartment(int $department_id, array $product_definitions): array
    {
        return $this->product_targets_by_department[$department_id] ?? [];
    }
}

function fake_daily_report_department(int $id, string $name, array $variables = []): object
{
    return (object)[
        'id' => $id,
        'name' => new FakeDailyReportValue($name),
        'variables' => new FakeDailyReportVariables($variables),
    ];
}

beforeEach(function (): void {
    $_SERVER['REQUEST_URI'] = '/departments/daily-reports/overview';
});

it('builds the overview payload from batched repository data with deterministic tile states', function (): void {
    $repository = new FakeDailyReportRepository();
    $complaints_repository = new FakeDailyReportComplaintsRepository();
    $repository->transaction_summary = [
        'quantity' => 12,
        'products' => 37,
        'earnings' => 4900,
        'washes' => 14,
        'water_usage' => 56,
    ];
    $repository->booking_summary = [
        'completed' => 9,
        'total' => 11,
    ];
    $repository->product_overview = [
        24 => ['product_id' => 24, 'quantity' => 3, 'out_of' => 14],
        25 => ['product_id' => 25, 'quantity' => 2, 'out_of' => 14],
    ];
    $complaints_repository->count = 4;
    $route = new DepartmentDailyReportsOverviewRouteDouble();
    $route->repository = $repository;
    $route->complaints_repository = $complaints_repository;
    $route->outside_hours_service = new FakeOutsideHoursStatisticsService();
    $route->outside_hours_service->summary = [
        'total' => 1,
        'by_source' => [
            'orders' => 0,
            'xlvask' => 1,
            'selfserve' => 0,
        ],
        'has_missing_opening_hours' => false,
        'missing_department_ids' => [],
    ];
    $route->departments = [
        fake_daily_report_department(1, 'North', ['workfeed_department_id' => 'dep_1']),
        fake_daily_report_department(2, 'South', ['workfeed_department_id' => 'dep_2']),
    ];
    $route->workfeed_shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'start' => '2026-03-23T09:00:00+00:00',
            'end' => '2026-03-23T17:30:00+00:00',
            'approval' => (object)['originalEnd' => '2026-03-23T17:00:00+00:00'],
        ],
        (object)[
            'departmentID' => 'dep_2',
            'start' => '2026-03-23T10:00:00+00:00',
            'end' => '2026-03-23T18:00:00+00:00',
            'approval' => null,
            'updateTime' => '2026-03-23T18:15:00+00:00',
        ],
    ];

    $overview = department_daily_reports_route_invoke_private($route, 'buildDailyReportOverview', [[1, 2], '2026-03-23', '2026-03-23']);

    expect($overview['department_ids'])->toBe([1, 2]);
    expect($overview['metrics']['transactions']['value'])->toBe(12);
    expect($overview['metrics']['bookings']['value'])->toBe(9);
    expect($overview['metrics']['bookings']['out_of'])->toBe(11);
    expect($overview['metrics']['night_washes']['state'])->toBe('ready');
    expect($overview['metrics']['night_washes']['value'])->toBe(1);
    expect($overview['metrics']['night_washes']['by_source'])->toBe([
        'orders' => 0,
        'xlvask' => 1,
        'selfserve' => 0,
    ]);
    expect($overview['metrics']['overtime']['state'])->toBe('ready');
    expect($overview['metrics']['overtime']['value'])->toBe(0.75);
    expect($overview['metrics']['complaints']['state'])->toBe('ready');
    expect($overview['metrics']['complaints']['value'])->toBe(4);
    expect(count($overview['products']))->toBe(6);
    expect($overview['products'][0]['slug'])->toBe('spot-free-lastbil');
    expect($overview['products'][0]['title'])->toBe('Spot Free (Lastbil)');
    expect($overview['products'][0]['value'])->toBe(3);
    expect($overview['products'][0]['target_percentage'])->toBeNull();
    expect($overview['products'][0]['target_department_id'])->toBeNull();
    expect($overview['products'][1]['title'])->toBe('Fælg flex pr. enhed');
    expect($overview['products'][1]['value'])->toBe(2);
    expect(array_column($overview['products'], 'title'))->toBe([
        'Spot Free (Lastbil)',
        'Fælg flex pr. enhed',
        'Ekstraordinær pr. 10 min inkl. kemi',
        'Højglans - Voksforsegling pr. enhed',
        'Undervognsskyl pr. enhed',
        'Tillæg for Specialsæbe - DD',
    ]);
});

it('adds product targets to single department overview payloads when requested', function (): void {
    $repository = new FakeDailyReportRepository();
    $repository->product_overview = [
        24 => ['product_id' => 24, 'quantity' => 3, 'out_of' => 14],
        25 => ['product_id' => 25, 'quantity' => 2, 'out_of' => 14],
    ];

    $route = new DepartmentDailyReportsOverviewRouteDouble();
    $route->repository = $repository;
    $route->complaints_repository = new FakeDailyReportComplaintsRepository();
    $route->outside_hours_service = new FakeOutsideHoursStatisticsService();
    $route->product_targets_by_department = [
        7 => [
            24 => 75.5,
            25 => 0.0,
        ],
    ];

    $overview = department_daily_reports_route_invoke_private($route, 'buildDailyReportOverview', [[7], '2026-03-23', '2026-03-23', true]);
    $products_by_id = [];
    foreach ($overview['products'] as $product) {
        $products_by_id[$product['product_id']] = $product;
    }

    expect($products_by_id[24]['target_percentage'])->toBe(75.5);
    expect($products_by_id[24]['target_department_id'])->toBe(7);
    expect($products_by_id[25]['target_percentage'])->toBe(0.0);
    expect($products_by_id[25]['target_department_id'])->toBe(7);
    expect($products_by_id[27]['target_percentage'])->toBeNull();
    expect($products_by_id[27]['target_department_id'])->toBeNull();
});

it('marks overtime unavailable when not every selected department can be mapped to workfeed', function (): void {
    $repository = new FakeDailyReportRepository();

    $route = new DepartmentDailyReportsOverviewRouteDouble();
    $route->repository = $repository;
    $route->complaints_repository = new FakeDailyReportComplaintsRepository();
    $route->outside_hours_service = new FakeOutsideHoursStatisticsService();
    $route->departments = [
        fake_daily_report_department(1, 'North', ['workfeed_department_id' => 'dep_1']),
        fake_daily_report_department(2, 'South'),
    ];

    $overview = department_daily_reports_route_invoke_private($route, 'buildDailyReportOverview', [[1, 2], '2026-03-23', '2026-03-23']);

    expect($overview['metrics']['overtime']['state'])->toBe('unavailable');
    expect($overview['metrics']['overtime']['message'])->toContain('Workfeed');
});

it('normalizes department id input from csv strings and nested values', function (): void {
    $route = new DepartmentDailyReportsOverviewRouteDouble();
    $route->repository = new FakeDailyReportRepository();
    $route->complaints_repository = new FakeDailyReportComplaintsRepository();
    $route->outside_hours_service = new FakeOutsideHoursStatisticsService();

    $normalized = department_daily_reports_route_invoke_private($route, 'normalizeDepartmentIdsParameter', [['1, 2', [3, '4'], 2]]);

    expect($normalized)->toBe([1, 2, 3, 4]);
});

it('limits overtime counting to the selected reporting range', function (): void {
    $route = new DepartmentDailyReportsOverviewRouteDouble();
    $route->repository = new FakeDailyReportRepository();
    $route->complaints_repository = new FakeDailyReportComplaintsRepository();
    $route->outside_hours_service = new FakeOutsideHoursStatisticsService();

    $hours = department_daily_reports_route_invoke_private($route, 'calculateShiftOvertimeHoursInRange', [[
        'start' => '2026-03-22T20:00:00+00:00',
        'end' => '2026-03-23T00:30:00+00:00',
        'approval' => (object)['originalEnd' => '2026-03-22T23:45:00+00:00'],
    ], new \DateTime('2026-03-23T00:00:00+00:00'), new \DateTime('2026-03-24T00:00:00+00:00')]);

    expect($hours)->toBe(0.5);
});

it('does not count negative approved overtime when a saved end shortens the shift', function (): void {
    $route = new DepartmentDailyReportsOverviewRouteDouble();
    $route->repository = new FakeDailyReportRepository();
    $route->complaints_repository = new FakeDailyReportComplaintsRepository();
    $route->outside_hours_service = new FakeOutsideHoursStatisticsService();

    $hours = department_daily_reports_route_invoke_private($route, 'calculateShiftOvertimeHoursInRange', [[
        'start' => '2026-03-23T10:00:00+00:00',
        'end' => '2026-03-23T17:00:00+00:00',
        'approval' => (object)['originalEnd' => '2026-03-23T18:00:00+00:00'],
    ], new \DateTime('2026-03-23T00:00:00+00:00'), new \DateTime('2026-03-24T00:00:00+00:00')]);

    expect($hours)->toBe(0.0);
});

it('ignores late unapproved administrative edits when calculating overtime', function (): void {
    $route = new DepartmentDailyReportsOverviewRouteDouble();
    $route->repository = new FakeDailyReportRepository();
    $route->complaints_repository = new FakeDailyReportComplaintsRepository();
    $route->outside_hours_service = new FakeOutsideHoursStatisticsService();

    $hours = department_daily_reports_route_invoke_private($route, 'calculateShiftOvertimeHoursInRange', [[
        'start' => '2026-03-23T09:00:00+00:00',
        'end' => '2026-03-23T17:00:00+00:00',
        'approval' => null,
        'updateTime' => '2026-03-24T02:30:00+00:00',
    ], new \DateTime('2026-03-23T00:00:00+00:00'), new \DateTime('2026-03-24T00:00:00+00:00')]);

    expect($hours)->toBe(0.0);
});

it('wires the overview route to batched repository methods and overview path', function (): void {
    $routeContent = (string)file_get_contents(app_path('routes/departmentDailyReportsRoute.php'));
    $objectContent = (string)file_get_contents(app_path('objects/department_daily_reports_o.php'));

    expect($routeContent)->toContain('/departments/daily-reports/overview');
    expect($routeContent)->toContain('/departments/daily-reports/product-targets');
    expect($routeContent)->toContain('/superuser/departments/{id}/overview');
    expect($routeContent)->toContain('superuser_fetch_department');
    expect($routeContent)->toContain('set_department_daily_report_product_targets');
    expect($routeContent)->toContain('/departments/daily-reports/complaints');
    expect($routeContent)->toContain('outsideHoursStatisticsService');
    expect($routeContent)->toContain('dailyReportComplaintsRepository');
    expect($routeContent)->toContain('/departments/daily-reports/outside-hours-trend');
    expect($routeContent)->toContain('getTransactionSummaryForDepartments');
    expect($routeContent)->toContain('normalizeDepartmentIdsParameter');
    expect($objectContent)->toContain('department_wash_count_service');
    expect($objectContent)->toContain('countInDateRange');
    expect($objectContent)->toContain('listTransactions');
    expect($objectContent)->toContain('public function getBookingSummaryForDepartments');
});
