#!/usr/bin/env php
<?php

declare(strict_types=1);

function assert_true(bool $cond, string $msg): void
{
    if (!$cond) {
        fwrite(STDERR, "[FAIL] {$msg}\n");
        exit(1);
    }
}

function request(string $method, string $route, array $json = [], array $headers = []): array|WP_Error
{
    $req = new WP_REST_Request($method, $route, $json, $headers);
    return rest_do_request($req);
}

function unwrap(array|WP_Error $res, string $context): array
{
    if ($res instanceof WP_Error) {
        $status = $res->data['status'] ?? 500;
        fwrite(STDERR, "[FAIL] {$context}: {$res->code} {$res->message} ({$status})\n");
        exit(1);
    }
    return $res;
}

require __DIR__ . '/wp_emulation.php';

$health = unwrap(request('GET', '/calendar/v1/health'), 'health');
assert_true(($health['status'] ?? '') === 'ok', 'health status should be ok');

$GLOBALS['wp_user'] = [
    'id' => 0,
    'email' => '',
    'caps' => [],
];
$unauthCreate = request('POST', '/calendar/v1/events', [
    'title' => 'No Auth Event',
    'start_datetime' => '2026-05-01T09:00:00+01:00',
    'end_datetime' => '2026-05-01T10:00:00+01:00',
]);
assert_true($unauthCreate instanceof WP_Error, 'unauthenticated event create should be denied');
assert_true(($unauthCreate->data['status'] ?? 0) === 403, 'unauthenticated event create should return 403');

$GLOBALS['wp_user'] = [
    'id' => 1,
    'email' => 'admin@example.test',
    'caps' => ['manage_options', 'edit_posts'],
];

$settingsPatch = unwrap(
    request('PATCH', '/calendar/v1/settings', ['caldav_calendar_name' => 'Calendar E2E']),
    'patch settings'
);
assert_true(($settingsPatch['data']['caldav_calendar_name'] ?? '') === 'Calendar E2E', 'settings patch must persist');

$settingsGet = unwrap(request('GET', '/calendar/v1/settings'), 'get settings');
assert_true(($settingsGet['data']['caldav_calendar_name'] ?? '') === 'Calendar E2E', 'settings get should reflect patch');

$register = unwrap(
    request('POST', '/calendar/v1/users/register', ['email' => 'demo@example.test', 'password' => 'pass12345', 'debug_tokens' => true]),
    'register user'
);
$verifyToken = (string) ($register['data']['verify_token'] ?? '');
assert_true($verifyToken !== '', 'register should issue verify token');
$userId = (int) (($register['data']['user']['id'] ?? 0));
assert_true($userId > 0, 'register should create user id');

$loginPending = request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'pass12345']);
assert_true($loginPending instanceof WP_Error, 'pending user login should fail');
assert_true(($loginPending->data['status'] ?? 0) === 401, 'pending user login should return 401');

$verified = unwrap(request('POST', '/calendar/v1/users/verify', ['token' => $verifyToken]), 'verify email');
assert_true(str_contains((string) ($verified['data']['message'] ?? ''), 'admin will review'), 'verify should return approval message');

$approved = unwrap(request('PATCH', '/calendar/v1/admin/users/' . $userId . '/approve', []), 'approve user');
assert_true((string) ($approved['data']['account_status'] ?? '') === 'active', 'approved user status should be active');

$loginActive = unwrap(
    request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'pass12345']),
    'active user login'
);
assert_true((bool) (($loginActive['data']['ok'] ?? false) === true), 'active user login should succeed');

$resetReq = unwrap(
    request('POST', '/calendar/v1/users/password/request', ['email' => 'demo@example.test', 'debug_tokens' => true]),
    'request reset'
);
$resetToken = (string) ($resetReq['data']['reset_token'] ?? '');
assert_true($resetToken !== '', 'password reset request should return reset token');

$resetDone = unwrap(
    request('POST', '/calendar/v1/users/password/reset', ['token' => $resetToken, 'new_password' => 'newpass123']),
    'reset password'
);
assert_true((bool) (($resetDone['data']['ok'] ?? false) === true), 'password reset should succeed');

$loginNewPass = unwrap(
    request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'newpass123']),
    'login with new password'
);
assert_true((bool) (($loginNewPass['data']['ok'] ?? false) === true), 'login should work after password reset');

$resetReuse = request('POST', '/calendar/v1/users/password/reset', ['token' => $resetToken, 'new_password' => 'anotherpass123']);
assert_true($resetReuse instanceof WP_Error, 'password reset token should be single-use');
assert_true(($resetReuse->data['status'] ?? 0) === 422, 'reused reset token should return 422');

$usersList = unwrap(request('GET', '/calendar/v1/admin/users'), 'list users');
assert_true(count((array) ($usersList['data'] ?? [])) >= 1, 'admin users list should return at least one row');

$removeUser = unwrap(request('DELETE', '/calendar/v1/admin/users/' . $userId), 'remove user');
assert_true((bool) (($removeUser['data']['deleted'] ?? false) === true), 'admin remove user should succeed');
$loginRemoved = request('POST', '/calendar/v1/users/login', ['email' => 'demo@example.test', 'password' => 'newpass123']);
assert_true($loginRemoved instanceof WP_Error, 'removed user login should fail');
assert_true(($loginRemoved->data['status'] ?? 0) === 401, 'removed user login should return 401');

$created = unwrap(
    request('POST', '/calendar/v1/events', [
        'title' => 'Emulated Recurring Event',
        'description' => 'e2e',
        'location' => 'Room 1',
        'category' => 'Test',
        'all_day_event' => false,
        'start_datetime' => '2026-04-01T10:00:00+01:00',
        'end_datetime' => '2026-04-01T11:00:00+01:00',
        'repeat_type' => 'daily',
        'repeat_interval' => 1,
        'repeat_range_mode' => 'count',
        'repeat_count' => 3,
    ]),
    'create event'
);
$eventId = (int) ($created['data']['id'] ?? 0);
assert_true($eventId > 0, 'event id should be generated');

$invalidEvent = request('POST', '/calendar/v1/events', [
    'title' => 'Bad Range',
    'start_datetime' => '2026-04-12T12:00:00+01:00',
    'end_datetime' => '2026-04-12T11:00:00+01:00',
]);
assert_true($invalidEvent instanceof WP_Error, 'invalid event with end before start should fail');
assert_true(($invalidEvent->data['status'] ?? 0) === 422, 'invalid event should return 422');

$public = unwrap(request('GET', '/calendar/v1/public/events', ['view' => 'month', 'date' => '2026-04-01']), 'public events');
$countBefore = (int) ($public['meta']['count'] ?? 0);
assert_true($countBefore >= 3, 'public month view should include recurrence occurrences');

$preview = unwrap(
    request('POST', '/calendar/v1/events/preview-occurrences', [
        'event' => [
            'title' => 'Preview Event',
            'start_datetime' => '2026-04-05T08:00:00+01:00',
            'end_datetime' => '2026-04-05T09:00:00+01:00',
            'repeat_type' => 'weekly',
            'repeat_interval' => 1,
            'repeat_range_mode' => 'count',
            'repeat_count' => 4,
        ],
        'from' => '2026-04-01',
        'months' => 1,
    ]),
    'preview occurrences'
);
assert_true((int) ($preview['meta']['count'] ?? 0) >= 1, 'preview occurrences should return generated items');

$previewBad = request('POST', '/calendar/v1/events/preview-occurrences', [
    'event' => [
        'title' => 'Bad Preview',
        'start_datetime' => '2026-04-05T10:00:00+01:00',
        'end_datetime' => '2026-04-05T09:00:00+01:00',
        'repeat_type' => 'weekly',
    ],
    'from' => '2026-04-01',
    'months' => 1,
]);
assert_true($previewBad instanceof WP_Error, 'preview occurrences should validate invalid event range');
assert_true(($previewBad->data['status'] ?? 0) === 422, 'invalid preview payload should return 422');

$deleteOne = unwrap(
    request('DELETE', '/calendar/v1/events/' . $eventId . '/occurrences/' . rawurlencode('2026-04-02T10:00:00+01:00')),
    'delete occurrence'
);
assert_true(($deleteOne['data']['deleted'] ?? false) === true, 'delete occurrence should succeed');

$deleteSameAgain = unwrap(
    request('DELETE', '/calendar/v1/events/' . $eventId . '/occurrences/' . rawurlencode('2026-04-02T10:00:00+01:00')),
    'delete same occurrence again'
);
assert_true(($deleteSameAgain['data']['deleted'] ?? false) === true, 'repeat delete occurrence should be idempotent success');

$occurrences = unwrap(
    request('GET', '/calendar/v1/events/' . $eventId . '/occurrences', ['from' => '2026-04-01', 'months' => 1]),
    'event occurrences'
);
assert_true((int) ($occurrences['meta']['count'] ?? 0) >= 2, 'event occurrences endpoint should return remaining recurrence instances');

$publicAfter = unwrap(request('GET', '/calendar/v1/public/events', ['view' => 'month', 'date' => '2026-04-01']), 'public events after delete');
$days = [];
foreach (($publicAfter['data'] ?? []) as $item) {
    $days[] = substr((string) ($item['occurrence_start'] ?? ''), 0, 10);
}
assert_true(!in_array('2026-04-02', $days, true), 'deleted single occurrence should be excluded');

$ics = unwrap(request('GET', '/calendar/v1/public/ics'), 'public ics');
$icsBody = (string) ($ics['data'] ?? '');
assert_true(str_contains($icsBody, 'BEGIN:VCALENDAR'), 'ics response should include vcalendar envelope');
assert_true(str_contains($icsBody, 'RRULE:'), 'ics response should include recurrence rule');
assert_true(str_contains($icsBody, 'EXDATE'), 'ics response should include deleted occurrence as exdate');

$monthlyOrdinal = unwrap(
    request('POST', '/calendar/v1/events', [
        'title' => 'Monthly Ordinal',
        'description' => 'monthly weekday parity',
        'location' => 'Room 2',
        'category' => 'Monthly',
        'all_day_event' => false,
        'start_datetime' => '2026-04-05T09:30:00+01:00',
        'end_datetime' => '2026-04-05T10:30:00+01:00',
        'repeat_type' => 'monthly',
        'repeat_interval' => 1,
        'repeat_nth_mode' => 'weekday_of_month',
        'repeat_nth_pos' => -1,
        'repeat_nth_weekday' => 0,
        'repeat_range_mode' => 'count',
        'repeat_count' => 3,
    ]),
    'create monthly ordinal event'
);
$monthlyEventId = (int) ($monthlyOrdinal['data']['id'] ?? 0);
assert_true($monthlyEventId > 0, 'monthly ordinal event id should be generated');
$monthlyOcc = unwrap(
    request('GET', '/calendar/v1/events/' . $monthlyEventId . '/occurrences', ['from' => '2026-04-01', 'months' => 3]),
    'list monthly ordinal occurrences'
);
$monthlyDays = array_map(
    static fn(array $row): string => substr((string) ($row['occurrence_start'] ?? ''), 0, 10),
    (array) ($monthlyOcc['data'] ?? [])
);
assert_true(in_array('2026-04-26', $monthlyDays, true), 'monthly ordinal should include last Sunday of April 2026');
assert_true(in_array('2026-05-31', $monthlyDays, true), 'monthly ordinal should include last Sunday of May 2026');

$resources = unwrap(request('GET', '/calendar/v1/caldav/resources'), 'list caldav resources');
$resourceList = (array) ($resources['data'] ?? []);
assert_true(count($resourceList) >= 1, 'caldav resources should list at least one object');
$firstResource = (string) ($resourceList[0]['resource'] ?? '');
assert_true($firstResource !== '', 'caldav resource should have object name');

$getObject = unwrap(request('GET', '/calendar/v1/caldav/object/' . rawurlencode($firstResource)), 'get caldav object');
$firstEtag = (string) ($getObject['data']['etag'] ?? '');
assert_true($firstEtag !== '', 'caldav object should include etag');

$newResource = 'new-fixture-event.ics';
$newIcs = implode("\r\n", [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    'PRODID:-//Calendar Plugin E2E//EN',
    'BEGIN:VEVENT',
    'UID:new-fixture-event@calendar-plugin',
    'SUMMARY:Fixture PUT Event',
    'DESCRIPTION:Created via CalDAV PUT',
    'DTSTART;TZID=Europe/London:20260410T093000',
    'DTEND;TZID=Europe/London:20260410T103000',
    'END:VEVENT',
    'END:VCALENDAR',
    '',
]);
$createdViaPut = unwrap(
    request('PUT', '/calendar/v1/caldav/object/' . $newResource, ['ics' => $newIcs], ['If-None-Match' => '*']),
    'caldav put create'
);
assert_true((bool) (($createdViaPut['data']['created'] ?? false) === true), 'caldav put should create object with if-none-match');

$duplicateBlocked = request(
    'PUT',
    '/calendar/v1/caldav/object/' . $newResource,
    ['ics' => $newIcs],
    ['If-None-Match' => '*']
);
assert_true($duplicateBlocked instanceof WP_Error, 'duplicate create should fail on if-none-match precondition');
assert_true(($duplicateBlocked->data['status'] ?? 0) === 412, 'duplicate create should return 412');

$updatedIcs = implode("\r\n", [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    'PRODID:-//Calendar Plugin E2E//EN',
    'BEGIN:VEVENT',
    'UID:new-fixture-event@calendar-plugin',
    'SUMMARY:Fixture PUT Event Updated',
    'DESCRIPTION:Updated via CalDAV PUT',
    'DTSTART;TZID=Europe/London:20260410T100000',
    'DTEND;TZID=Europe/London:20260410T110000',
    'END:VEVENT',
    'END:VCALENDAR',
    '',
]);
$staleUpdate = request(
    'PUT',
    '/calendar/v1/caldav/object/' . $newResource,
    ['ics' => $updatedIcs],
    ['If-Match' => '"stale-etag"']
);
assert_true($staleUpdate instanceof WP_Error, 'stale etag update should fail with precondition error');
assert_true(($staleUpdate->data['status'] ?? 0) === 412, 'stale etag update should return 412');

$currentObj = unwrap(request('GET', '/calendar/v1/caldav/object/' . $newResource), 'get new resource object');
$currentEtag = (string) ($currentObj['data']['etag'] ?? '');
assert_true($currentEtag !== '', 'newly created object should have etag');

$updatedOk = unwrap(
    request('PUT', '/calendar/v1/caldav/object/' . $newResource, ['ics' => $updatedIcs], ['If-Match' => $currentEtag]),
    'caldav put update with etag'
);
assert_true((bool) (($updatedOk['data']['created'] ?? true) === false), 'caldav put with if-match should update existing object');

$multi = unwrap(
    request('POST', '/calendar/v1/caldav/multiget', ['resources' => [$newResource, 'missing-event.ics']]),
    'caldav multiget'
);
$multiData = (array) ($multi['data'] ?? []);
assert_true(count($multiData) === 2, 'multiget should return one row per requested resource');
assert_true((int) ($multiData[0]['status'] ?? 0) === 200, 'multiget should return 200 for existing resource');
assert_true((int) ($multiData[1]['status'] ?? 0) === 404, 'multiget should return 404 for missing resource');

$deletedObj = unwrap(request('DELETE', '/calendar/v1/caldav/object/' . $newResource), 'caldav delete object');
assert_true((bool) (($deletedObj['data']['deleted'] ?? false) === true), 'caldav delete should remove object');

$deletedNotFound = request('GET', '/calendar/v1/caldav/object/' . $newResource);
assert_true($deletedNotFound instanceof WP_Error, 'deleted caldav object should no longer resolve');
assert_true(($deletedNotFound->data['status'] ?? 0) === 404, 'deleted caldav object should return 404');

$sluggedSettings = unwrap(
    request('PATCH', '/calendar/v1/settings', ['url_slug' => 'events']),
    'patch slug setting'
);
assert_true(($sluggedSettings['data']['url_slug'] ?? '') === 'events', 'url_slug setting should persist');
$sluggedCalendarHtml = do_shortcode('[calendar]');
assert_true(str_contains($sluggedCalendarHtml, 'href="/events/calendar.ics"'), 'calendar shortcode should use slugged ICS path');
assert_true(str_contains($sluggedCalendarHtml, 'href="/events/caldav/"'), 'calendar shortcode should use slugged CalDAV path');

$sidebarHtml = do_shortcode('[calendar_sidebar_upcoming]');
assert_true(str_contains($sidebarHtml, '<div class="calendar-plugin-shell"'), 'sidebar shortcode should render');

echo "[PASS] compatibility-layer e2e wp emulation\n";
