<?php

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

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

    $service = new OperationRunService;

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

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

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

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

    $service = new 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('does not replace the initiator when deduping', function () {
    $tenant = Tenant::factory()->create();
    $userA = User::factory()->create();
    $userB = User::factory()->create();

    $service = new 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 = Tenant::factory()->create();
    $service = new 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 = Tenant::factory()->create();
    $service = new 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 = Tenant::factory()->create();
    $service = new OperationRunService;

    $fired = false;

    $dispatcher = OperationRun::getEventDispatcher();

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

        $fired = true;

        OperationRun::withoutEvents(function () use ($model): void {
            OperationRun::query()->create([
                'tenant_id' => $model->tenant_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('tenant_id', $tenant->getKey())->where('type', 'test.race')->count())
        ->toBe(1);
});

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

    $service = new 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('updates run lifecycle fields and summaries', function () {
    $tenant = Tenant::factory()->create();

    $service = new 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 = Tenant::factory()->create();
    $service = new 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]');
});
