<?php

app_require('classes/release_manager.php');

use classes\release_manager;

function releaseStatusOverviewForTest(array $summary): array
{
    $manager = new release_manager();
    $method = new ReflectionMethod(release_manager::class, 'releaseStatusOverview');

    return $method->invoke($manager, array_replace([
        'generated_at' => '2026-05-20T10:00:00+00:00',
        'channels' => [],
        'deployment_targets' => [],
        'service_sets' => [],
        'deployments' => [],
    ], $summary));
}

function releaseStatusChannelBySlug(array $overview, string $slug): array
{
    foreach ($overview['channels'] as $channel) {
        if (($channel['channel_slug'] ?? '') === $slug) {
            return $channel;
        }
    }

    throw new RuntimeException('Release status channel not found: ' . $slug);
}

function releaseStatusServiceByKey(array $channel, string $key): array
{
    foreach ($channel['services'] as $service) {
        if (($service['service_key'] ?? '') === $key) {
            return $service;
        }
    }

    throw new RuntimeException('Release status service not found: ' . $key);
}

function releaseStatusReadyService(string $kind, int $id): array
{
    return [
        'id' => $id,
        'resource_uuid' => $kind . '-resource',
        'resource_name' => ucfirst($kind),
        'deployment_status' => 'deployed',
        'availability_state' => 'failover_ready',
        'replication' => [
            'status' => 'ok',
            'last_status' => [
                'status' => 'ok',
                'blockers' => [],
            ],
        ],
    ];
}

function releaseStatusReadyVersions(int $bundleId = 7): array
{
    return [
        'bundle_id' => $bundleId,
        'bundle_label' => '#' . $bundleId,
        'frontend' => [
            'id' => 101,
            'version_label' => 'frontend-2026-05-20',
            'status' => 'active',
        ],
        'api' => [
            'id' => 102,
            'version_label' => 'api-2026-05-20',
            'status' => 'active',
        ],
    ];
}

it('marks a channel ready when all release services are healthy', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 1,
                'slug' => 'stable',
                'name' => 'Stable',
                'default_channel' => true,
                'enabled' => true,
                'frontend_base_url' => 'https://app.example.test',
                'api_base_url' => 'https://api.example.test',
                'versions' => releaseStatusReadyVersions(),
            ],
        ],
        'deployment_targets' => [
            ['id' => 11, 'channel_id' => 1, 'app' => 'frontend', 'repository' => 'truckwash/front-end-vue'],
            ['id' => 12, 'channel_id' => 1, 'app' => 'api', 'repository' => 'truckwash/backend-php'],
        ],
        'service_sets' => [
            [
                'id' => 21,
                'channel_id' => 1,
                'mode' => 'isolated_stack',
                'stack' => [
                    'database' => releaseStatusReadyService('database', 31),
                    'redis' => releaseStatusReadyService('redis', 32),
                    'minio' => releaseStatusReadyService('minio', 33),
                ],
            ],
        ],
        'deployments' => [
            ['id' => 41, 'channel_id' => 1, 'app' => 'frontend', 'status' => 'deployed'],
            ['id' => 42, 'channel_id' => 1, 'app' => 'api', 'status' => 'deployed'],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'stable');

    expect($overview['state'])->toBe('ready')
        ->and($overview['totals']['critical'])->toBe(0)
        ->and($overview['totals']['warning'])->toBe(0)
        ->and($overview['totals']['services'])->toBe(5)
        ->and($channel['readiness'])->toBe('ready')
        ->and($channel['services'])->toHaveCount(5);
});

it('does not block channels on unhealthy data targets when data services are production shared', function (): void {
    $database = releaseStatusReadyService('database', 71);
    $database['deployment_status'] = 'reconcile_failed';
    $database['availability_state'] = 'degraded';

    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 2,
                'slug' => 'beta',
                'name' => 'Beta',
                'default_channel' => false,
                'enabled' => true,
                'frontend_base_url' => 'https://beta.example.test',
                'api_base_url' => 'https://api-beta.example.test',
                'versions' => releaseStatusReadyVersions(8),
            ],
        ],
        'deployment_targets' => [
            ['id' => 51, 'channel_id' => 2, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-beta'],
            ['id' => 52, 'channel_id' => 2, 'app' => 'api', 'coolify_service_uuid' => 'api-beta'],
        ],
        'service_sets' => [
            [
                'id' => 53,
                'channel_id' => 2,
                'mode' => 'attach_existing',
                'stack' => [
                    'database' => $database,
                    'redis' => releaseStatusReadyService('redis', 75),
                    'minio' => releaseStatusReadyService('minio', 76),
                ],
            ],
        ],
        'deployments' => [
            ['id' => 61, 'channel_id' => 2, 'app' => 'frontend', 'status' => 'deployed'],
            ['id' => 62, 'channel_id' => 2, 'app' => 'api', 'status' => 'deployed'],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'beta');

    expect($channel['readiness'])->toBe('ready')
        ->and($channel['issues'])->toBe([])
        ->and(releaseStatusServiceByKey($channel, 'database'))->toMatchArray([
            'status' => 'production_shared',
            'state' => 'ready',
            'severity' => 'ok',
            'issue_type' => null,
        ]);
});

it('marks beta ready from the production frontend and API services', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 1,
                'slug' => 'stable',
                'name' => 'Stable',
                'default_channel' => true,
                'enabled' => true,
                'frontend_base_url' => 'https://app.example.test',
                'api_base_url' => 'https://api.example.test',
                'versions' => releaseStatusReadyVersions(),
            ],
            [
                'id' => 2,
                'slug' => 'beta',
                'name' => 'Beta',
                'default_channel' => false,
                'enabled' => true,
                'versions' => [],
                'availability' => [
                    'configured' => false,
                    'status' => 'unconfigured',
                    'missing' => ['frontend_version', 'frontend_base_url', 'api_version', 'api_base_url'],
                ],
            ],
        ],
        'deployment_targets' => [
            ['id' => 11, 'channel_id' => 1, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-prod'],
            ['id' => 12, 'channel_id' => 1, 'app' => 'api', 'coolify_service_uuid' => 'api-prod'],
        ],
        'deployments' => [
            ['id' => 41, 'channel_id' => 1, 'app' => 'frontend', 'status' => 'deployed'],
            ['id' => 42, 'channel_id' => 1, 'app' => 'api', 'status' => 'deployed'],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'beta');

    expect($channel['readiness'])->toBe('ready')
        ->and($channel['service_policy'])->toBe('production_shared')
        ->and($channel['service_channel_slug'])->toBe('stable')
        ->and($channel['missing_values'])->toBe([])
        ->and($channel['availability'])->toMatchArray([
            'configured' => true,
            'missing' => [],
            'frontend_base_url' => 'https://app.example.test',
            'api_base_url' => 'https://api.example.test',
        ])
        ->and(releaseStatusServiceByKey($channel, 'frontend'))->toMatchArray([
            'status' => 'production_shared',
            'service_policy' => 'production_shared',
            'service_channel_slug' => 'stable',
        ])
        ->and(releaseStatusServiceByKey($channel, 'api'))->toMatchArray([
            'status' => 'production_shared',
            'service_policy' => 'production_shared',
            'service_channel_slug' => 'stable',
        ]);
});

it('ignores missing legacy bundles but still blocks on missing versions and URLs', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 2,
                'slug' => 'beta',
                'name' => 'Beta',
                'default_channel' => false,
                'enabled' => true,
                'versions' => [],
                'availability' => [
                    'configured' => false,
                    'status' => 'unconfigured',
                    'missing' => [
                        'release_bundle',
                        'frontend_version',
                        'frontend_base_url',
                        'api_version',
                        'api_base_url',
                    ],
                ],
            ],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'beta');
    $missingLabels = array_column($channel['missing_values'], 'label');
    $missingKeys = array_column($channel['missing_values'], 'key');

    expect($overview['state'])->toBe('blocked')
        ->and($overview['totals']['critical'])->toBeGreaterThanOrEqual(4)
        ->and($channel['readiness'])->toBe('blocked')
        ->and($missingKeys)->not->toContain('release_bundle')
        ->and($missingLabels)->not->toContain('release bundle')
        ->and($missingLabels)->toContain('frontend version')
        ->and($missingLabels)->toContain('frontend URL')
        ->and($missingLabels)->toContain('API version')
        ->and($missingLabels)->toContain('API URL')
        ->and($overview['issues'][0])->toMatchArray([
            'severity' => 'critical',
            'type' => 'missing_value',
            'channel_slug' => 'beta',
        ]);
});

it('marks a branch-based channel ready without a release bundle when app versions and URLs exist', function (): void {
    $versions = releaseStatusReadyVersions(0);
    unset($versions['bundle_id'], $versions['bundle_label']);

    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 2,
                'slug' => 'beta',
                'name' => 'Beta',
                'default_channel' => false,
                'enabled' => true,
                'frontend_base_url' => 'https://api-v2.truckwash.io/beta/frontend',
                'api_base_url' => 'https://api-v2.truckwash.io/beta/api',
                'versions' => $versions,
            ],
        ],
        'deployment_targets' => [
            ['id' => 21, 'channel_id' => 2, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-beta'],
            ['id' => 22, 'channel_id' => 2, 'app' => 'api', 'coolify_service_uuid' => 'api-beta'],
        ],
        'deployments' => [
            ['id' => 61, 'channel_id' => 2, 'app' => 'frontend', 'status' => 'deployed'],
            ['id' => 62, 'channel_id' => 2, 'app' => 'api', 'status' => 'deployed'],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'beta');

    expect($overview['state'])->toBe('ready')
        ->and($channel['readiness'])->toBe('ready')
        ->and($channel['availability'])->toMatchArray([
            'configured' => true,
            'missing' => [],
            'status' => 'ready',
        ])
        ->and($channel['issues'])->toBe([])
        ->and($channel['missing_values'])->toBe([]);
});

it('does not report frontend or API URLs missing when target auto endpoints are resolvable', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 2,
                'slug' => 'beta',
                'name' => 'Beta',
                'default_channel' => false,
                'enabled' => true,
                'versions' => releaseStatusReadyVersions(),
            ],
        ],
        'deployment_targets' => [
            [
                'id' => 21,
                'channel_id' => 2,
                'channel_slug' => 'beta',
                'app' => 'frontend',
                'repository' => 'truckwash/front-end-vue',
                'branch' => 'master',
                'coolify_service_uuid' => 'frontend-beta-service',
                'deploy_context' => [
                    'endpoint_mode' => 'auto',
                    'public_gateway_host' => 'gateway.example.test',
                ],
            ],
            [
                'id' => 22,
                'channel_id' => 2,
                'channel_slug' => 'beta',
                'app' => 'api',
                'repository' => 'truckwash/backend-php',
                'branch' => 'master',
                'coolify_service_uuid' => 'api-beta-service',
                'deploy_context' => [
                    'endpoint_mode' => 'auto',
                    'public_gateway_host' => 'gateway.example.test',
                ],
            ],
        ],
        'service_sets' => [
            [
                'id' => 31,
                'channel_id' => 2,
                'mode' => 'isolated_stack',
                'stack' => [
                    'database' => releaseStatusReadyService('database', 41),
                    'redis' => releaseStatusReadyService('redis', 42),
                    'minio' => releaseStatusReadyService('minio', 43),
                ],
            ],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'beta');
    $missingKeys = array_column($channel['missing_values'], 'key');

    expect($missingKeys)->not->toContain('frontend_base_url')
        ->and($missingKeys)->not->toContain('api_base_url')
        ->and($channel['availability'])->toMatchArray([
            'configured' => true,
            'frontend_base_url' => 'https://gateway.example.test/beta/frontend',
            'api_base_url' => 'https://gateway.example.test/beta/api',
        ])
        ->and($channel['readiness'])->toBe('ready');
});

it('does not surface legacy release bundle actions as readiness blockers', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 20,
                'slug' => 'beta',
                'name' => 'Beta',
                'default_channel' => false,
                'enabled' => true,
                'frontend_base_url' => 'https://beta.example.test',
                'api_base_url' => 'https://api-beta.example.test',
                'versions' => [],
                'availability' => [
                    'configured' => false,
                    'status' => 'unconfigured',
                    'missing' => ['release_bundle'],
                ],
            ],
        ],
        'bundles' => [
            ['id' => 91, 'channel_id' => 20, 'status' => 'deployed', 'version_label' => 'beta-bundle'],
            ['id' => 92, 'channel_id' => 20, 'status' => 'failed', 'version_label' => 'failed-bundle'],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'beta');

    expect($channel['readiness'])->toBe('ready')
        ->and($channel['availability'])->toMatchArray([
            'configured' => true,
            'missing' => [],
            'status' => 'ready',
        ])
        ->and($channel['issues'])->toBe([])
        ->and($channel['missing_values'])->toBe([]);
});

it('surfaces failed latest deployments as critical promotion blockers', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 3,
                'slug' => 'canary',
                'name' => 'Canary',
                'default_channel' => false,
                'enabled' => true,
                'frontend_base_url' => 'https://canary.example.test',
                'api_base_url' => 'https://api-canary.example.test',
                'versions' => releaseStatusReadyVersions(9),
                'availability' => [
                    'configured' => true,
                    'status' => 'ready',
                    'missing' => [],
                ],
            ],
        ],
        'deployment_targets' => [
            ['id' => 51, 'channel_id' => 3, 'app' => 'frontend', 'repository' => 'truckwash/front-end-vue', 'coolify_service_uuid' => 'frontend-canary'],
            ['id' => 52, 'channel_id' => 3, 'app' => 'api', 'repository' => 'truckwash/backend-php', 'coolify_service_uuid' => 'api-canary'],
        ],
        'service_sets' => [
            [
                'id' => 53,
                'channel_id' => 3,
                'mode' => 'isolated_stack',
                'stack' => [
                    'database' => releaseStatusReadyService('database', 54),
                    'redis' => releaseStatusReadyService('redis', 55),
                    'minio' => releaseStatusReadyService('minio', 56),
                ],
            ],
        ],
        'deployments' => [
            [
                'id' => 57,
                'channel_id' => 3,
                'app' => 'api',
                'status' => 'failed',
                'failure_summary' => [
                    'root_cause' => 'Composer install failed.',
                    'next_action' => 'Open the deployment logs and fix composer dependencies.',
                ],
            ],
            ['id' => 58, 'channel_id' => 3, 'app' => 'frontend', 'status' => 'deployed'],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'canary');
    $api = releaseStatusServiceByKey($channel, 'api');

    expect($overview['state'])->toBe('blocked')
        ->and($channel['readiness'])->toBe('blocked')
        ->and($api)->toMatchArray([
            'severity' => 'critical',
            'state' => 'failed',
            'issue_type' => 'failed_deployment',
            'deployment_id' => 57,
        ])
        ->and($channel['issues'][0])->toMatchArray([
            'severity' => 'critical',
            'type' => 'failed_deployment',
            'deployment_id' => 57,
            'next_action' => 'Open the deployment logs and fix composer dependencies.',
        ])
        ->and($channel['issues'][0]['key'])->toBe('failed_deployment:3:api::57:')
        ->and($channel['issues'][0]['impact'])->toContain('cannot be promoted')
        ->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('retry_deployment');
});

it('offers application target preparation for path routed Coolify failures', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 30,
                'slug' => 'internal',
                'name' => 'Internal',
                'default_channel' => false,
                'enabled' => true,
                'frontend_base_url' => 'https://internal.example.test',
                'api_base_url' => 'https://api-internal.example.test',
                'versions' => releaseStatusReadyVersions(19),
                'availability' => [
                    'configured' => true,
                    'status' => 'ready',
                    'missing' => [],
                ],
            ],
        ],
        'deployment_targets' => [
            ['id' => 81, 'channel_id' => 30, 'app' => 'frontend', 'repository' => 'truckwash/front-end-vue', 'coolify_service_uuid' => 'frontend-internal'],
            ['id' => 82, 'channel_id' => 30, 'app' => 'api', 'repository' => 'truckwash/backend-php', 'coolify_service_uuid' => 'legacy-api-service'],
        ],
        'service_sets' => [
            [
                'id' => 83,
                'channel_id' => 30,
                'mode' => 'isolated_stack',
                'stack' => [
                    'database' => releaseStatusReadyService('database', 84),
                    'redis' => releaseStatusReadyService('redis', 85),
                    'minio' => releaseStatusReadyService('minio', 86),
                ],
            ],
        ],
        'deployments' => [
            [
                'id' => 87,
                'channel_id' => 30,
                'app' => 'api',
                'status' => 'failed',
                'failure_summary' => [
                    'root_cause' => 'Path-routed release targets require a Coolify application resource with StripPrefix labels.',
                    'next_action' => 'Set coolify_resource_type=application or migrate this target before deploying.',
                ],
            ],
            ['id' => 88, 'channel_id' => 30, 'app' => 'frontend', 'status' => 'deployed'],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'internal');
    $actions = $channel['issues'][0]['actions'];
    $actionIds = array_column($actions, 'id');
    $prepareAction = $actions[array_search('prepare_application_target', $actionIds, true)];

    expect($actionIds)->toContain('retry_deployment')
        ->and($actionIds)->toContain('prepare_application_target')
        ->and($prepareAction)->toMatchArray([
            'requires_confirmation' => true,
            'requires_input' => false,
            'disabled_reason' => '',
        ]);
});

it('flags isolated stacks that are missing database Redis and MinIO services', function (): void {
    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 4,
                'slug' => 'internal',
                'name' => 'Internal',
                'default_channel' => false,
                'enabled' => true,
                'frontend_base_url' => 'https://internal.example.test',
                'api_base_url' => 'https://api-internal.example.test',
                'versions' => releaseStatusReadyVersions(11),
                'availability' => [
                    'configured' => true,
                    'status' => 'ready',
                    'missing' => [],
                ],
            ],
        ],
        'deployment_targets' => [
            ['id' => 61, 'channel_id' => 4, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-internal'],
            ['id' => 62, 'channel_id' => 4, 'app' => 'api', 'coolify_service_uuid' => 'api-internal'],
        ],
        'service_sets' => [
            [
                'id' => 63,
                'channel_id' => 4,
                'mode' => 'isolated_stack',
                'stack' => [
                    'frontend' => ['id' => 61],
                    'api' => ['id' => 62],
                ],
                'data_services' => [],
            ],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'internal');

    expect($channel['readiness'])->toBe('blocked')
        ->and(releaseStatusServiceByKey($channel, 'database'))->toMatchArray([
            'severity' => 'critical',
            'issue_type' => 'missing_value',
            'missing_key' => 'database_service',
        ])
        ->and(releaseStatusServiceByKey($channel, 'redis'))->toMatchArray([
            'severity' => 'critical',
            'issue_type' => 'missing_value',
            'missing_key' => 'redis_service',
        ])
        ->and(releaseStatusServiceByKey($channel, 'minio'))->toMatchArray([
            'severity' => 'critical',
            'issue_type' => 'missing_value',
            'missing_key' => 'minio_service',
        ])
        ->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('complete_data_services');
});

it('maps degraded Coolify targets and reconcile failures to release service issues', function (): void {
    $database = releaseStatusReadyService('database', 71);
    $database['deployment_status'] = 'reconcile_failed';
    $database['availability_state'] = 'degraded';

    $overview = releaseStatusOverviewForTest([
        'channels' => [
            [
                'id' => 5,
                'slug' => 'preview',
                'name' => 'Preview',
                'default_channel' => false,
                'enabled' => true,
                'frontend_base_url' => 'https://preview.example.test',
                'api_base_url' => 'https://api-preview.example.test',
                'versions' => releaseStatusReadyVersions(12),
                'availability' => [
                    'configured' => true,
                    'status' => 'ready',
                    'missing' => [],
                ],
            ],
        ],
        'deployment_targets' => [
            ['id' => 72, 'channel_id' => 5, 'app' => 'frontend', 'coolify_service_uuid' => 'frontend-preview'],
            ['id' => 73, 'channel_id' => 5, 'app' => 'api', 'coolify_service_uuid' => 'api-preview'],
        ],
        'service_sets' => [
            [
                'id' => 74,
                'channel_id' => 5,
                'mode' => 'isolated_stack',
                'stack' => [
                    'database' => $database,
                    'redis' => releaseStatusReadyService('redis', 75),
                    'minio' => releaseStatusReadyService('minio', 76),
                ],
            ],
        ],
    ]);

    $channel = releaseStatusChannelBySlug($overview, 'preview');
    $databaseRow = releaseStatusServiceByKey($channel, 'database');

    expect($databaseRow)->toMatchArray([
        'severity' => 'critical',
        'state' => 'service_unhealthy',
        'issue_type' => 'service_unhealthy',
        'coolify_target_id' => 71,
    ])
        ->and($channel['issues'][0])->toMatchArray([
            'severity' => 'critical',
            'type' => 'service_unhealthy',
            'service_key' => 'database',
        ])
        ->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('reconcile_coolify_target')
        ->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('redeploy_coolify_target')
        ->and(array_column($channel['issues'][0]['actions'], 'id'))->toContain('restart_coolify_target');
});
