<?php

app_require('routes/moduleWeatherAPIRoute.php');

use classes\workfeed;
use classes\redis;
use routes\moduleWeatherAPIRoute;

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

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

beforeEach(function (): void {
    $_SERVER['REQUEST_URI'] = '/departments/weather';
});

it('calculates workfeed employee hours for the hour slot based on overlap', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-24T13:00:00+00:00');
    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
        (object)[
            'departmentID' => 'dep_1',
            'checkIn' => (object)['time' => '2026-03-24T13:30:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T15:00:00+00:00'],
            'end' => '2026-03-24T15:00:00+00:00',
        ],
        (object)[
            'departmentID' => 'dep_other',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
        (object)[
            'departmentID' => 'dep_1',
            'checkIn' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'end' => '2026-03-24T13:00:00+00:00',
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);

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

it('calculates weather hour contributions grouped per employee for a slot', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-24T13:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'employeeID' => 'emp_1',
            'employeeName' => 'Alice',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
        (object)[
            'departmentID' => 'dep_1',
            'employeeID' => 'emp_1',
            'employeeName' => 'Alice',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T13:30:00+00:00'],
            'end' => '2026-03-24T13:30:00+00:00',
        ],
        (object)[
            'departmentID' => 'dep_1',
            'employeeID' => 'emp_2',
            'employeeName' => 'Bob',
            'checkIn' => (object)['time' => '2026-03-24T13:15:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
        (object)[
            'departmentID' => 'dep_other',
            'employeeID' => 'emp_3',
            'employeeName' => 'Ignored',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
    ];

    $details = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHourByEmployee', [$shifts, 'dep_1', $slot]);

    expect($details)->toBe([
        [
            'employee_id' => 'emp_1',
            'employee_name' => 'Alice',
            'hours' => 1.5,
        ],
        [
            'employee_id' => 'emp_2',
            'employee_name' => 'Bob',
            'hours' => 0.75,
        ],
    ]);
});

it('calculates weather hour contributions for canonical nested workfeed employee schema', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-24T13:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'employee' => (object)[
                'id' => '005FnnP0fHohybM1f3tx',
                'firstname' => 'Michael',
                'lastname' => 'Stenbæk Stampe',
            ],
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
    ];

    $details = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHourByEmployee', [$shifts, 'dep_1', $slot]);

    expect($details)->toBe([
        [
            'employee_id' => '005FnnP0fHohybM1f3tx',
            'employee_name' => 'Michael Stenbæk Stampe',
            'hours' => 1.0,
        ],
    ]);
});

it('extracts weather employee identity from supported shift payload shapes', function (): void {
    $route = new moduleWeatherAPIRoute();

    $fromTopLevel = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
        (object)[
            'employeeID' => 'emp_1',
            'employeeName' => 'Alice',
        ],
    ]);

    $fromNested = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
        (object)[
            'employee' => (object)[
                'id' => 'emp_2',
                'firstName' => 'Bob',
                'lastName' => 'Builder',
            ],
        ],
    ]);

    $fromCanonicalNestedSchema = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
        (object)[
            'employee' => (object)[
                'id' => 'emp_3',
                'firstname' => 'Charlie',
                'lastname' => 'Day',
            ],
        ],
    ]);

    expect($fromTopLevel)->toBe([
        'id' => 'emp_1',
        'name' => 'Alice',
    ]);
    expect($fromNested)->toBe([
        'id' => 'emp_2',
        'name' => 'Bob Builder',
    ]);
    expect($fromCanonicalNestedSchema)->toBe([
        'id' => 'emp_3',
        'name' => 'Charlie Day',
    ]);
});

it('prefers canonical workfeed employee schema fields over generic employee names', function (): void {
    $route = new moduleWeatherAPIRoute();

    $identity = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
        (object)[
            'employee' => (object)[
                'id' => 'emp_4',
                'firstname' => 'Dana',
                'lastname' => 'Scully',
                'name' => 'Wrong Name',
            ],
            'employeeName' => 'Also Wrong',
        ],
    ]);

    expect($identity)->toBe([
        'id' => 'emp_4',
        'name' => 'Dana Scully',
    ]);
});

it('does not use workfeed schema name fields as employee ids', function (): void {
    $route = new moduleWeatherAPIRoute();

    $identity = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
        (object)[
            'employee' => (object)[
                'firstname' => 'Fox',
                'lastname' => 'Mulder',
            ],
        ],
    ]);

    expect($identity)->toBe([
        'id' => null,
        'name' => 'Fox Mulder',
    ]);
});

it('returns a null employee name when no workfeed employee name is available', function (): void {
    $route = new moduleWeatherAPIRoute();

    $identity = weather_route_invoke_private($route, 'extractWorkfeedEmployeeIdentity', [
        (object)[
            'employeeID' => 'emp_99',
        ],
    ]);

    expect($identity)->toBe([
        'id' => 'emp_99',
        'name' => null,
    ]);
});

it('resolves missing workfeed employee names from the employees endpoint', function (): void {
    $route = new moduleWeatherAPIRoute();

    $workfeed = new class extends workfeed {
        public function __construct()
        {
        }

        public function listEmployees(array $filters = []): array|object
        {
            return [
                (object)[
                    'id' => 'emp_42',
                    'firstname' => 'Jane',
                    'lastname' => 'Doe',
                ],
            ];
        }

        public function getEmployee(string $id): object
        {
            return (object)[];
        }
    };

    $resolvedNames = [];
    $employees = weather_route_invoke_private($route, 'resolveMissingWorkfeedEmployeeNames', [[
        [
            'employee_id' => 'emp_42',
            'employee_name' => null,
            'hours' => 1.0,
        ],
    ], null, &$resolvedNames, $workfeed]);

    expect($employees)->toBe([[
        'employee_id' => 'emp_42',
        'employee_name' => 'Jane Doe',
        'hours' => 1.0,
    ]]);
});

it('filters employee hour rows that cannot be resolved to a workfeed schema name', function (): void {
    $route = new moduleWeatherAPIRoute();

    $resolvedNames = [];
    $employees = weather_route_invoke_private($route, 'resolveMissingWorkfeedEmployeeNames', [[
        [
            'employee_id' => 'emp_missing',
            'employee_name' => null,
            'hours' => 1.0,
        ],
    ], null, &$resolvedNames, null]);

    expect($employees)->toBe([]);
});

it('resolves employee display name from cache when shift payload lacks a name', function (): void {
    $route = new moduleWeatherAPIRoute();

    $cache = new class([
        'emp_42' => 'Jane Doe',
    ]) extends redis {
        private array $namesByEmployeeId;

        public function __construct(array $namesByEmployeeId)
        {
            $this->namesByEmployeeId = $namesByEmployeeId;
        }

        public function get_workfeed_employee_name(string $employeeId): string|null
        {
            return $this->namesByEmployeeId[$employeeId] ?? null;
        }
    };

    $resolved_name = weather_route_invoke_private($route, 'fetchCachedWorkfeedEmployeeDisplayName', [$cache, 'emp_42']);

    expect($resolved_name)->toBe('Jane Doe');
});

it('extracts department id from supported workfeed shift shapes', function (): void {
    $route = new moduleWeatherAPIRoute();

    $idFromNested = weather_route_invoke_private($route, 'extractWorkfeedDepartmentId', [
        (object)[
            'department' => (object)['id' => 'dep_nested'],
            'start' => '2026-03-24T12:45:00+00:00',
            'end' => '2026-03-24T13:15:00+00:00',
        ],
    ]);

    $idFromCamelCase = weather_route_invoke_private($route, 'extractWorkfeedDepartmentId', [[
        'departmentId' => 'dep_camel',
        'start' => '2026-03-24T12:45:00+00:00',
        'end' => '2026-03-24T13:15:00+00:00',
    ]]);

    expect($idFromNested)->toBe('dep_nested');
    expect($idFromCamelCase)->toBe('dep_camel');
});

it('normalizes wrapped workfeed collections from common response keys', function (): void {
    $route = new moduleWeatherAPIRoute();

    $wrapped = (object)[
        'data' => [
            (object)['id' => 'a'],
            (object)['id' => 'b'],
        ],
    ];

    $items = weather_route_invoke_private($route, 'normalizeWorkfeedCollection', [$wrapped]);

    expect($items)->toHaveCount(2);
    expect($items[0]->id ?? null)->toBe('a');
    expect($items[1]->id ?? null)->toBe('b');
});

it('calculates workfeed employee hours across multiple departments for one hour slot', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-24T13:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
        (object)[
            'departmentID' => 'dep_2',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => (object)['time' => '2026-03-24T14:00:00+00:00'],
            'end' => '2026-03-24T14:00:00+00:00',
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, ['dep_1', 'dep_2'], $slot]);

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

it('does not count shifts without punches when checkIn/checkOut are null', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-24T13:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'checkIn' => null,
            'checkOut' => null,
            'start' => '2026-03-24T13:00:00+00:00',
            'end' => '2026-03-24T14:00:00+00:00',
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);

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

it('counts checked-in shifts without checkOut up to occurredUntil', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-24T13:00:00+00:00');
    $occurredUntil = new DateTime('2026-03-24T13:40:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'checkIn' => (object)['time' => '2026-03-24T13:10:00+00:00'],
            'checkOut' => null,
            'end' => '2026-03-24T16:00:00+00:00',
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot, $occurredUntil]);

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

it('counts overtime minutes when a saved shift end extends past the approved original end', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-23T18:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'start' => '2026-03-23T10:00:00+00:00',
            'end' => '2026-03-23T18:17:00+00:00',
            'approval' => (object)[
                'originalEnd' => '2026-03-23T18:00:00+00:00',
            ],
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);

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

it('does not count removed approved time when a saved shift end is shortened', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-23T17:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            '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',
            ],
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);

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

it('counts unapproved overtime from a bounded shift updateTime fallback', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-23T17:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'start' => '2026-03-23T09:00:00+00:00',
            'end' => '2026-03-23T17:00:00+00:00',
            'approval' => null,
            'updateTime' => '2026-03-23T17:15:00+00:00',
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);

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

it('does not treat late unapproved administrative edits as overtime', function (): void {
    $route = new moduleWeatherAPIRoute();
    $slot = new DateTime('2026-03-23T17:00:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            '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',
        ],
    ];

    $hours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $slot]);

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

it('caps current-slot hours to elapsed minutes and zeroes future slots', function (): void {
    $route = new moduleWeatherAPIRoute();
    $currentSlot = new DateTime('2026-03-24T13:00:00+00:00');
    $futureSlot = new DateTime('2026-03-24T14:00:00+00:00');
    $occurredUntil = new DateTime('2026-03-24T13:15:00+00:00');

    $shifts = [
        (object)[
            'departmentID' => 'dep_1',
            'checkIn' => (object)['time' => '2026-03-24T13:00:00+00:00'],
            'checkOut' => null,
            'end' => '2026-03-24T15:00:00+00:00',
        ],
    ];

    $currentHours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $currentSlot, $occurredUntil]);
    $futureHours = weather_route_invoke_private($route, 'calculateWorkfeedEmployeeHoursForHour', [$shifts, 'dep_1', $futureSlot, $occurredUntil]);

    expect($currentHours)->toBe(0.25);
    expect($futureHours)->toBe(0.0);
});

it('normalizes department id input from scalar csv and nested array values', function (): void {
    $route = new moduleWeatherAPIRoute();

    $single = weather_route_invoke_private($route, 'normalizeDepartmentIdInput', ['7']);
    $csv = weather_route_invoke_private($route, 'normalizeDepartmentIdInput', ['1, 2,3']);
    $nested = weather_route_invoke_private($route, 'normalizeDepartmentIdInput', [[1, '2,3', [4, '5']]]);

    expect($single)->toBe(['7']);
    expect($csv)->toBe(['1', '2', '3']);
    expect($nested)->toBe([1, '2', '3', 4, '5']);
});

it('normalizes batched wash count rows into hourly slot totals', function (): void {
    $route = new moduleWeatherAPIRoute();
    $rows = [
        [
            'department_id' => 1,
            'hour_bucket' => '2026-03-24 08:00:00',
            'wash_count' => 3,
        ],
        [
            'department_id' => 2,
            'hour_bucket' => '2026-03-24 08:15:00',
            'wash_count' => 2,
        ],
        [
            'department_id' => 2,
            'hour_bucket' => '2026-03-24 09:00:00',
            'wash_count' => 4,
        ],
    ];

    $counts = weather_route_invoke_private($route, 'normalizeDepartmentWashCountRows', [$rows]);

    expect($counts)->toBe([
        '2026-03-24 08:00' => 5,
        '2026-03-24 09:00' => 4,
    ]);
});

it('wires department weather route to use batched wash aggregation', function (): void {
    $routeContent = (string)file_get_contents(app_path('routes/moduleWeatherAPIRoute.php'));
    $ordersContent = (string)file_get_contents(app_path('objects/orders_o.php'));

    expect($routeContent)->toContain('loadDepartmentWashCountsBySlot');
    expect($routeContent)->toContain('countWashesByHourForDepartments');
    expect($ordersContent)->toContain('public function countWashesByHourForDepartments');
    expect($ordersContent)->toContain('department_wash_count_service');
});
