<?php

use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\OperationRunService;

function operationRunService(): OperationRunService
{
    return app(OperationRunService::class);
}

it('creates a new operation run', function () {
    $tenant = ManagedEnvironment::factory()->create();
    $user = User::factory()->create();

    $service = operationRunService();

    $run = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $user);

    expect($run)->toBeInstanceOf(OperationRun::class);

    $this->assertDatabaseHas('operation_runs', [
        'id' => $run->getKey(),
        'managed_environment_id' => $tenant->getKey(),
        'type' => 'test.action',
        'status' => 'queued',
        'initiator_name' => $user->name,
    ]);
});

it('reuses an active run (idempotent)', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $service = operationRunService();

    $runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']);
    $runB = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']);

    expect($runA->getKey())->toBe($runB->getKey());
    expect(OperationRun::query()->count())->toBe(1);
});

it('dedupes assignment run identities by type and scope', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $service = operationRunService();

    $fetchRunA = $service->ensureRunWithIdentity(
        tenant: $tenant,
        type: 'assignments.fetch',
        identityInputs: [
            'backup_item_id' => 101,
        ],
        context: [
            'backup_item_id' => 101,
            'phase' => 'capture',
        ],
    );

    $fetchRunB = $service->ensureRunWithIdentity(
        tenant: $tenant,
        type: 'assignments.fetch',
        identityInputs: [
            'backup_item_id' => 101,
        ],
        context: [
            'backup_item_id' => 101,
            'phase' => 'capture-again',
        ],
    );

    $fetchRunDifferentScope = $service->ensureRunWithIdentity(
        tenant: $tenant,
        type: 'assignments.fetch',
        identityInputs: [
            'backup_item_id' => 102,
        ],
        context: [
            'backup_item_id' => 102,
            'phase' => 'capture',
        ],
    );

    $restoreRunA = $service->ensureRunWithIdentity(
        tenant: $tenant,
        type: 'assignments.restore',
        identityInputs: [
            'restore_run_id' => 501,
        ],
        context: [
            'restore_run_id' => 501,
            'phase' => 'execute',
        ],
    );

    $restoreRunB = $service->ensureRunWithIdentity(
        tenant: $tenant,
        type: 'assignments.restore',
        identityInputs: [
            'restore_run_id' => 501,
        ],
        context: [
            'restore_run_id' => 501,
            'phase' => 'execute-again',
        ],
    );

    expect($fetchRunA->getKey())->toBe($fetchRunB->getKey());
    expect($restoreRunA->getKey())->toBe($restoreRunB->getKey());
    expect($fetchRunA->getKey())->not->toBe($fetchRunDifferentScope->getKey());
    expect($fetchRunA->getKey())->not->toBe($restoreRunA->getKey());
});

it('does not replace the initiator when deduping', function () {
    $tenant = ManagedEnvironment::factory()->create();
    $userA = User::factory()->create();
    $userB = User::factory()->create();

    $service = operationRunService();

    $runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $userA);
    $runB = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $userB);

    expect($runA->getKey())->toBe($runB->getKey());
    expect($runB->fresh()?->user_id)->toBe($userA->getKey());
    expect($runB->fresh()?->initiator_name)->toBe($userA->name);
});

it('hashes inputs deterministically regardless of key order', function () {
    $tenant = ManagedEnvironment::factory()->create();
    $service = operationRunService();

    $runA = $service->ensureRun($tenant, 'test.action', ['b' => 2, 'a' => 1]);
    $runB = $service->ensureRun($tenant, 'test.action', ['a' => 1, 'b' => 2]);

    expect($runA->getKey())->toBe($runB->getKey());
});

it('hashes list inputs deterministically regardless of list order', function () {
    $tenant = ManagedEnvironment::factory()->create();
    $service = operationRunService();

    $runA = $service->ensureRun($tenant, 'test.action', ['ids' => [2, 1]]);
    $runB = $service->ensureRun($tenant, 'test.action', ['ids' => [1, 2]]);

    expect($runA->getKey())->toBe($runB->getKey());
});

it('handles unique-index race collisions by returning the active run', function () {
    $tenant = ManagedEnvironment::factory()->create();
    $service = operationRunService();

    $fired = false;

    $dispatcher = OperationRun::getEventDispatcher();

    OperationRun::creating(function (OperationRun $model) use (&$fired, $tenant): void {
        if ($fired) {
            return;
        }

        $fired = true;

        OperationRun::withoutEvents(function () use ($model, $tenant): void {
            OperationRun::query()->create([
                'workspace_id' => (int) $tenant->workspace_id,
                'managed_environment_id' => $model->managed_environment_id,
                'user_id' => $model->user_id,
                'initiator_name' => $model->initiator_name,
                'type' => $model->type,
                'status' => $model->status,
                'outcome' => $model->outcome,
                'run_identity_hash' => $model->run_identity_hash,
                'context' => $model->context,
            ]);
        });
    });

    try {
        $run = $service->ensureRun($tenant, 'test.race', ['scope' => 'full']);
    } finally {
        OperationRun::flushEventListeners();
        OperationRun::setEventDispatcher($dispatcher);
    }

    expect($run)->toBeInstanceOf(OperationRun::class);
    expect(OperationRun::query()->where('managed_environment_id', $tenant->getKey())->where('type', 'test.race')->count())
        ->toBe(1);
});

it('creates a new run after the previous one completed', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $service = operationRunService();

    $runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']);
    $runA->update(['status' => 'completed']);

    $runB = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']);

    expect($runA->getKey())->not->toBe($runB->getKey());
    expect(OperationRun::query()->count())->toBe(2);
});

it('reuses the same run even after completion when using strict identity', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $service = operationRunService();

    $runA = $service->ensureRunWithIdentityStrict(
        tenant: $tenant,
        type: 'backup_schedule_run',
        identityInputs: ['backup_schedule_id' => 123, 'scheduled_for' => '2026-01-05 10:00:00'],
        context: ['backup_schedule_id' => 123, 'scheduled_for' => '2026-01-05 10:00:00'],
    );

    $runA->update(['status' => 'completed', 'outcome' => 'succeeded']);

    $runB = $service->ensureRunWithIdentityStrict(
        tenant: $tenant,
        type: 'backup_schedule_run',
        identityInputs: ['backup_schedule_id' => 123, 'scheduled_for' => '2026-01-05 10:00:00'],
        context: ['backup_schedule_id' => 123, 'scheduled_for' => '2026-01-05 10:00:00'],
    );

    expect($runA->getKey())->toBe($runB->getKey());
    expect(OperationRun::query()->count())->toBe(1);
});

it('handles strict unique-index race collisions by returning the existing run', function () {
    $tenant = ManagedEnvironment::factory()->create();
    $service = operationRunService();

    $fired = false;

    $dispatcher = OperationRun::getEventDispatcher();

    OperationRun::creating(function (OperationRun $model) use (&$fired, $tenant): void {
        if ($fired) {
            return;
        }

        $fired = true;

        OperationRun::withoutEvents(function () use ($model, $tenant): void {
            OperationRun::query()->create([
                'workspace_id' => (int) $tenant->workspace_id,
                'managed_environment_id' => $model->managed_environment_id,
                'user_id' => $model->user_id,
                'initiator_name' => $model->initiator_name,
                'type' => $model->type,
                'status' => $model->status,
                'outcome' => $model->outcome,
                'run_identity_hash' => $model->run_identity_hash,
                'context' => $model->context,
            ]);
        });
    });

    try {
        $run = $service->ensureRunWithIdentityStrict(
            tenant: $tenant,
            type: 'backup_schedule_run',
            identityInputs: ['backup_schedule_id' => 123, 'scheduled_for' => '2026-01-05 10:00:00'],
            context: ['backup_schedule_id' => 123, 'scheduled_for' => '2026-01-05 10:00:00'],
        );
    } finally {
        OperationRun::flushEventListeners();
        OperationRun::setEventDispatcher($dispatcher);
    }

    expect($run)->toBeInstanceOf(OperationRun::class);
    expect(OperationRun::query()->where('managed_environment_id', $tenant->getKey())->where('type', 'backup_schedule_run')->count())
        ->toBe(1);
});

it('updates run lifecycle fields and summaries', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $service = operationRunService();

    $run = $service->ensureRun($tenant, 'test.action', []);

    $service->updateRun($run, 'running');

    $fresh = $run->fresh();
    expect($fresh?->status)->toBe('running');
    expect($fresh?->started_at)->not->toBeNull();
    $service->updateRun($run, 'completed', 'succeeded', ['succeeded' => 1]);

    $fresh = $run->fresh();
    expect($fresh?->status)->toBe('completed');
    expect($fresh?->outcome)->toBe('succeeded');
    expect($fresh?->completed_at)->not->toBeNull();
    expect($fresh?->summary_counts)->toBe(['succeeded' => 1]);
});

it('sanitizes failure messages and redacts obvious secrets', function () {
    $tenant = ManagedEnvironment::factory()->create();
    $service = operationRunService();

    $run = $service->ensureRun($tenant, 'test.action', []);

    try {
        throw new RuntimeException('Authorization: Bearer abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
    } catch (Throwable $e) {
        $service->failRun($run, $e);
    }

    $fresh = $run->fresh();
    expect($fresh?->status)->toBe('completed');
    expect($fresh?->outcome)->toBe('failed');
    expect($fresh?->failure_summary)->toBeArray();

    $message = (string) (($fresh?->failure_summary[0]['message'] ?? ''));
    expect($message)->not->toContain('abcdefghijklmnopqrstuvwxyz');
    expect($message)->toContain('[REDACTED');
});
