<?php

declare(strict_types=1);

use App\Exceptions\Supplier\SupplierAuthException;
use App\Exceptions\Supplier\SupplierClientException;
use App\Exceptions\Supplier\SupplierTransientException;
use App\Mail\SupplierCriticalAlertMail;
use App\Models\Project;
use App\Models\SupplierManualSyncQueue;
use App\Models\Tenant;
use App\Services\Supplier\Channel\Exceptions\TierEscalatedException;
use App\Services\Supplier\Channel\Exceptions\WindowDeferredException;
use App\Services\Supplier\Channel\FailoverProjectChannel;
use App\Services\Supplier\Channel\SupplierProjectChannel;
use App\Services\Supplier\Dto\SupplierProjectDto;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;

uses(DatabaseTransactions::class);

function makeDto(): SupplierProjectDto
{
    return new SupplierProjectDto(
        platform: 'B1', signalType: 'site', uniqueKey: 'foo.com',
        limit: 10, workdays: [1, 2], regions: [], regionsReverse: false, status: 'active',
    );
}

function makeFailover(SupplierProjectChannel $tier1, ?SupplierProjectChannel $tier2 = null): FailoverProjectChannel
{
    return new FailoverProjectChannel(
        $tier1,
        $tier2 ?? new class implements SupplierProjectChannel
        {
            public function createProject(SupplierProjectDto $dto): int
            {
                throw new RuntimeException('tier2 not configured');
            }

            public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

            public function listProjects(): array
            {
                return [];
            }
        },
        app(Mailer::class),
    );
}

beforeEach(function (): void {
    Mail::fake();
});

it('createProject — Tier 1 success: returns id, no queue, no alert', function (): void {
    $tier1 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            return 700123;
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };

    $id = makeFailover($tier1)->createProject(makeDto());

    expect($id)->toBe(700123);
    expect(SupplierManualSyncQueue::count())->toBe(0);
    Mail::assertNothingQueued();
});

it('createProject — Tier 1 transient-exhausted: skips Tier 2, jumps to Tier 3 with portal_unreachable', function (): void {
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->for($tenant)->create(['signal_type' => 'site', 'signal_identifier' => 'foo.com']);

    $tier1 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            throw new SupplierTransientException('5xx exhausted', httpStatus: 503);
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };
    $tier2Called = false;
    $tier2 = new class($tier2Called) implements SupplierProjectChannel
    {
        public function __construct(public bool &$called) {}

        public function createProject(SupplierProjectDto $dto): int
        {
            $this->called = true;

            return 0;
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void
        {
            $this->called = true;
        }

        public function listProjects(): array
        {
            $this->called = true;

            return [];
        }
    };

    expect(fn () => makeFailover($tier1, $tier2)->createProjectForLiderra($project, makeDto()))
        ->toThrow(TierEscalatedException::class);

    expect($tier2Called)->toBeFalse();
    expect(SupplierManualSyncQueue::where('project_id', $project->id)->where('failure_reason', 'portal_unreachable')->count())->toBe(1);
    Mail::assertQueued(SupplierCriticalAlertMail::class);
});

it('createProject — Tier 1 client-exc → Tier 2 success: no queue', function (): void {
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->for($tenant)->create();

    $tier1 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            throw new SupplierClientException('4xx contract break', httpStatus: 400);
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };
    $tier2 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            return 800001;
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };

    $id = makeFailover($tier1, $tier2)->createProjectForLiderra($project, makeDto());

    expect($id)->toBe(800001);
    expect(SupplierManualSyncQueue::count())->toBe(0);
    Mail::assertQueued(SupplierCriticalAlertMail::class); // failover_to_form alert
});

it('createProject — Tier 1 client-exc + Tier 2 fail: Tier 3 queue, manual_required alert', function (): void {
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->for($tenant)->create();

    $tier1 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            throw new SupplierClientException('4xx', httpStatus: 400);
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };
    $tier2 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            throw new RuntimeException('form_selector_break');
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };

    expect(fn () => makeFailover($tier1, $tier2)->createProjectForLiderra($project, makeDto()))
        ->toThrow(TierEscalatedException::class);

    expect(SupplierManualSyncQueue::where('project_id', $project->id)->where('status', 'pending')->count())->toBe(1);
    Mail::assertQueued(SupplierCriticalAlertMail::class);
});

it('createProject — Tier 1 auth-exc → Tier 2 success', function (): void {
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->for($tenant)->create();

    $tier1 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            throw new SupplierAuthException('sticky 401', httpStatus: 401);
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };
    $tier2 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            return 900042;
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };

    $id = makeFailover($tier1, $tier2)->createProjectForLiderra($project, makeDto());

    expect($id)->toBe(900042);
});

it('createProject — WindowDeferred: no queue, no escalation, op rescheduled (re-throws WindowDeferred)', function (): void {
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->for($tenant)->create();

    $tier1 = new class implements SupplierProjectChannel
    {
        public function createProject(SupplierProjectDto $dto): int
        {
            throw new WindowDeferredException('portal returned 22:00-00:00 window-block');
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [];
        }
    };

    expect(fn () => makeFailover($tier1)->createProjectForLiderra($project, makeDto()))
        ->toThrow(WindowDeferredException::class);

    expect(SupplierManualSyncQueue::count())->toBe(0);
    Mail::assertNothingQueued();
});

it('createProject — portal already has project (listProjects match): adopts external_id, skips create', function (): void {
    $tenant = Tenant::factory()->create();
    $project = Project::factory()->for($tenant)->create();

    $tier1CreateCalled = false;
    $tier1 = new class($tier1CreateCalled) implements SupplierProjectChannel
    {
        public function __construct(public bool &$createCalled) {}

        public function createProject(SupplierProjectDto $dto): int
        {
            $this->createCalled = true;

            return 0;
        }

        public function updateProject(int $externalId, SupplierProjectDto $dto): void {}

        public function listProjects(): array
        {
            return [
                ['id' => 555555, 'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'foo.com'],
            ];
        }
    };

    $id = makeFailover($tier1)->createProjectForLiderra($project, makeDto());

    expect($id)->toBe(555555);
    expect($tier1CreateCalled)->toBeFalse();
});
