<?php

use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Graph\GraphClientInterface;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\PendingCalls\TestCall;
use Tests\Support\AssertsNoOutboundHttp;
use Tests\Support\FailHardGraphClient;

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/

pest()->extend(Tests\TestCase::class)
    ->use(RefreshDatabase::class)
    ->in('Feature');

pest()->extend(Tests\TestCase::class)
    ->in('Unit');

pest()->extend(Tests\TestCase::class)
    ->in('Deprecation');

beforeEach(function () {
    putenv('INTUNE_TENANT_ID');
    unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
});

/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/

expect()->extend('toBeOne', function () {
    return $this->toBe(1);
});

function fakeIdToken(string $tenantId): string
{
    $header = rtrim(strtr(base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])), '+/', '-_'), '=');
    $payload = rtrim(strtr(base64_encode(json_encode(['tid' => $tenantId])), '+/', '-_'), '=');

    return $header.'.'.$payload.'.signature';
}

/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/

function something()
{
    // ..
}

/**
 * Spec test naming helper.
 *
 * Convention for focused runs:
 * - Prefix every Spec 081 test title with "Spec081 ".
 * - Keep filenames suffixed with "Spec081Test.php".
 * - Command: `vendor/bin/sail artisan test --compact --filter=Spec081`
 */
function spec081(string $description): string
{
    $normalized = trim($description);

    if ($normalized === '') {
        return 'Spec081';
    }

    return str_starts_with($normalized, 'Spec081 ')
        ? $normalized
        : 'Spec081 '.$normalized;
}

/**
 * Convenience wrapper for Spec 081 tests.
 */
function itSpec081(string $description, ?\Closure $closure = null): TestCall
{
    $call = it(spec081($description), $closure);
    $call->group('spec081');

    return $call;
}

function bindFailHardGraphClient(): void
{
    app()->instance(GraphClientInterface::class, new FailHardGraphClient);
}

function assertNoOutboundHttp(\Closure $callback): mixed
{
    return AssertsNoOutboundHttp::run($callback);
}

/**
 * @param  array<string, mixed>  $attributes
 */
function createInventorySyncOperationRun(Tenant $tenant, array $attributes = []): \App\Models\OperationRun
{
    $context = is_array($attributes['context'] ?? null) ? $attributes['context'] : [];

    if (array_key_exists('selection_hash', $attributes)) {
        if (is_string($attributes['selection_hash']) && $attributes['selection_hash'] !== '') {
            $context['selection_hash'] = $attributes['selection_hash'];
        }

        unset($attributes['selection_hash']);
    }

    if (array_key_exists('selection_payload', $attributes)) {
        if (is_array($attributes['selection_payload'])) {
            $context = array_merge($context, $attributes['selection_payload']);
        }

        unset($attributes['selection_payload']);
    }

    if (! isset($context['selection_hash']) || ! is_string($context['selection_hash']) || $context['selection_hash'] === '') {
        $context['selection_hash'] = hash('sha256', 'inventory-sync-selection-default');
    }

    if (! isset($context['policy_types']) || ! is_array($context['policy_types'])) {
        $context['policy_types'] = ['deviceConfiguration'];
    }

    if (! isset($context['categories']) || ! is_array($context['categories'])) {
        $context['categories'] = [];
    }

    if (! array_key_exists('include_foundations', $context)) {
        $context['include_foundations'] = false;
    }

    if (! array_key_exists('include_dependencies', $context)) {
        $context['include_dependencies'] = false;
    }

    $finishedAt = $attributes['finished_at'] ?? null;
    unset($attributes['finished_at']);

    $providedStatus = (string) ($attributes['status'] ?? 'success');
    $normalizedStatus = match ($providedStatus) {
        'pending', 'queued' => 'queued',
        'running' => 'running',
        'completed' => 'completed',
        default => 'completed',
    };

    $normalizedOutcome = match ($providedStatus) {
        'success' => 'succeeded',
        'partial' => 'partially_succeeded',
        'skipped' => 'blocked',
        'failed' => 'failed',
        'pending', 'queued', 'running' => 'pending',
        default => $normalizedStatus === 'completed' ? 'succeeded' : 'pending',
    };

    $attributes['type'] = (string) ($attributes['type'] ?? 'inventory_sync');
    $attributes['workspace_id'] = (int) ($attributes['workspace_id'] ?? $tenant->workspace_id);
    $attributes['status'] = in_array($providedStatus, ['queued', 'running', 'completed'], true)
        ? $providedStatus
        : $normalizedStatus;
    $attributes['outcome'] = (string) ($attributes['outcome'] ?? $normalizedOutcome);
    $attributes['context'] = array_merge($context, is_array($attributes['context'] ?? null) ? $attributes['context'] : []);

    if ($finishedAt !== null && ! array_key_exists('completed_at', $attributes)) {
        $attributes['completed_at'] = $finishedAt;
    }

    return \App\Models\OperationRun::factory()
        ->for($tenant)
        ->create($attributes);
}

/**
 * Create a completed inventory sync run with coverage proof in context.
 *
 * @param  array<string, string>  $statusByType  Example: ['deviceConfiguration' => 'succeeded']
 * @param  list<string>  $foundationTypes
 * @param  array<string, mixed>  $attributes
 */
function createInventorySyncOperationRunWithCoverage(
    Tenant $tenant,
    array $statusByType,
    array $foundationTypes = [],
    array $attributes = [],
): \App\Models\OperationRun {
    $context = is_array($attributes['context'] ?? null) ? $attributes['context'] : [];
    $inventory = is_array($context['inventory'] ?? null) ? $context['inventory'] : [];

    $inventory['coverage'] = \App\Support\Inventory\InventoryCoverage::buildPayload(
        statusByType: $statusByType,
        foundationTypes: $foundationTypes,
    );

    $context['inventory'] = $inventory;
    $attributes['context'] = $context;

    if (! array_key_exists('finished_at', $attributes) && ! array_key_exists('completed_at', $attributes)) {
        $attributes['finished_at'] = now();
    }

    $attributes['type'] ??= \App\Support\OperationRunType::InventorySync->value;
    $attributes['status'] ??= \App\Support\OperationRunStatus::Completed->value;
    $attributes['outcome'] ??= \App\Support\OperationRunOutcome::Succeeded->value;

    return createInventorySyncOperationRun($tenant, $attributes);
}

/**
 * @return array{0: User, 1: Tenant}
 */
function createUserWithTenant(
    ?Tenant $tenant = null,
    ?User $user = null,
    string $role = 'owner',
    ?string $workspaceRole = null,
    bool $ensureDefaultMicrosoftProviderConnection = true,
): array {
    $user ??= User::factory()->create();
    $tenant ??= Tenant::factory()->create();

    $workspaceRole ??= $role;

    $validWorkspaceRoles = array_map(
        static fn (\App\Support\Auth\WorkspaceRole $role): string => $role->value,
        \App\Support\Auth\WorkspaceRole::cases(),
    );

    if (! in_array($workspaceRole, $validWorkspaceRoles, true)) {
        $workspaceRole = \App\Support\Auth\WorkspaceRole::Owner->value;
    }

    $workspace = null;

    if ($tenant->workspace_id !== null) {
        $workspace = Workspace::query()->whereKey($tenant->workspace_id)->first();
    }

    if (! $workspace instanceof Workspace) {
        $workspace = Workspace::factory()->create();

        $tenant->forceFill([
            'workspace_id' => (int) $workspace->getKey(),
        ])->save();
    }

    WorkspaceMembership::query()->updateOrCreate([
        'workspace_id' => (int) $workspace->getKey(),
        'user_id' => (int) $user->getKey(),
    ], [
        'role' => $workspaceRole,
    ]);

    session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
    $user->forceFill(['last_workspace_id' => (int) $workspace->getKey()])->save();

    $user->tenants()->syncWithoutDetaching([
        $tenant->getKey() => ['role' => $role],
    ]);

    if ($ensureDefaultMicrosoftProviderConnection) {
        ensureDefaultProviderConnection($tenant, 'microsoft');
    }

    return [$user, $tenant];
}

/**
 * @return array{tenant: string}
 */
function filamentTenantRouteParams(Tenant $tenant): array
{
    return ['tenant' => (string) $tenant->external_id];
}

function ensureDefaultProviderConnection(Tenant $tenant, string $provider = 'microsoft'): ProviderConnection
{
    $connection = ProviderConnection::query()
        ->where('tenant_id', (int) $tenant->getKey())
        ->where('provider', $provider)
        ->orderByDesc('is_default')
        ->orderBy('id')
        ->first();

    if (! $connection instanceof ProviderConnection) {
        $connection = ProviderConnection::factory()->create([
            'tenant_id' => (int) $tenant->getKey(),
            'provider' => $provider,
            'entra_tenant_id' => (string) ($tenant->tenant_id ?? fake()->uuid()),
            'status' => 'connected',
            'health_status' => 'ok',
            'is_default' => true,
        ]);
    } else {
        $entraTenantId = trim((string) $connection->entra_tenant_id);

        $updates = [];
        if (! $connection->is_default) {
            $updates['is_default'] = true;
        }

        if ($connection->status !== 'connected') {
            $updates['status'] = 'connected';
        }

        if ($connection->health_status !== 'ok') {
            $updates['health_status'] = 'ok';
        }

        if ($entraTenantId === '') {
            $updates['entra_tenant_id'] = (string) ($tenant->tenant_id ?? fake()->uuid());
        }

        if ($updates !== []) {
            $connection->forceFill($updates)->save();
            $connection->refresh();
        }
    }

    $credential = $connection->credential()->first();

    if (! $credential instanceof ProviderCredential) {
        ProviderCredential::factory()->create([
            'provider_connection_id' => (int) $connection->getKey(),
            'type' => 'client_secret',
            'payload' => [
                'client_id' => fake()->uuid(),
                'client_secret' => fake()->sha1(),
            ],
        ]);

        $connection->refresh();
    }

    return $connection;
}
