<?php

app_require('classes/bird.php');

use classes\bird;

class BirdGateCallClientFake extends bird
{
    private string $workspaceIdValue = '';
    private string $channelIdValue = '';

    /** @var string[] */
    public array $statusQueue = [];
    /** @var string[] */
    public array $statusesSeen = [];
    /** @var string[] */
    public array $flashStatusQueue = [];
    /** @var string[] */
    public array $flashStatusesSeen = [];
    public int $hangupCalls = 0;
    /** @var array<int,array<string,mixed>> */
    public array $hangupPayloads = [];
    /** @var array<int,array<string,mixed>> */
    public array $createPayloads = [];
    /** @var array<int,array<string,mixed>> */
    public array $flashCreatePayloads = [];
    /** @var array<string,mixed>|null */
    public ?array $flashCreateResponse = null;

    /**
     * @param string[] $statusQueue
     */
    public function __construct(mixed $workspaceId = 'workspace_1', mixed $channelId = 'channel_1', array $statusQueue = ['accepted'])
    {
        $this->statusQueue = $statusQueue;
        $this->workspaceIdValue = is_string($workspaceId) ? $workspaceId : '';
        $this->channelIdValue = is_string($channelId) ? $channelId : '';
    }

    protected function getConfiguredWorkspaceId(): string
    {
        return $this->workspaceIdValue;
    }

    protected function getConfiguredChannelId(): string
    {
        return $this->channelIdValue;
    }

    public function createVoiceCall(string $workspaceId, string $channelId, array $payload): array|object|null
    {
        $this->createPayloads[] = [
            'workspaceId' => $workspaceId,
            'channelId' => $channelId,
            'payload' => $payload,
        ];
        return ['id' => 'call_123'];
    }

    public function getVoiceCall(string $workspaceId, string $channelId, string $callId): array|object|null
    {
        $status = array_shift($this->statusQueue);
        if (!is_string($status) || trim($status) === '') {
            $status = 'ringing';
        }
        $this->statusesSeen[] = $status;

        return [
            'id' => $callId,
            'status' => $status,
        ];
    }

    public function hangupVoiceCall(string $workspaceId, string $channelId, string $callId, array $payload = []): array|object|null
    {
        $this->hangupCalls++;
        $this->hangupPayloads[] = $payload;
        return ['status' => 'completed'];
    }

    public function createFlashCall(string $workspaceId, string $channelId, array $payload): array|object|null
    {
        $this->flashCreatePayloads[] = [
            'workspaceId' => $workspaceId,
            'channelId' => $channelId,
            'payload' => $payload,
        ];
        return $this->flashCreateResponse ?? [
            'id' => 'flash_123',
            'status' => 'starting',
            'from' => '+4532330288',
        ];
    }

    public function getFlashCall(string $workspaceId, string $channelId, string $callId): array|object|null
    {
        $status = array_shift($this->flashStatusQueue);
        if (!is_string($status) || trim($status) === '') {
            $status = 'ringing';
        }
        $this->flashStatusesSeen[] = $status;

        return [
            'id' => $callId,
            'status' => $status,
        ];
    }

    protected function waitForCallPollInterval(int $pollIntervalSeconds): void
    {
        // Avoid real sleeps in tests.
    }
}

class BirdGateCallFlashFallbackClientFake extends BirdGateCallClientFake
{
    public bool $shouldFlashFail = false;
    public int $flashCallAttempts = 0;
    public int $regularCallAttempts = 0;

    public function callGateViaFlashCall(int $countryCode, int $phone, int $ringTimeout): void
    {
        $this->flashCallAttempts++;
        if ($this->shouldFlashFail) {
            throw new \Exception('Flash call failed');
        }
    }

    public function callGateAndHangupWhenAccepted(int $countryCode, int $phone, int $timeout)
    {
        $this->regularCallAttempts++;
    }
}

it('fails fast when workspace id is missing for gate calls', function (): void {
    $client = new BirdGateCallClientFake(workspaceId: '', channelId: 'channel_1');

    expect(fn() => $client->callGateAndHangupWhenAccepted(45, 12345678, 10))
        ->toThrow(\Exception::class, 'Bird workspaceId is not configured for gate calls');
});

it('fails fast when channel id is missing for gate calls', function (): void {
    $client = new BirdGateCallClientFake(workspaceId: 'workspace_1', channelId: '');

    expect(fn() => $client->callGateAndHangupWhenAccepted(45, 12345678, 10))
        ->toThrow(\Exception::class, 'Bird channelId is not configured for gate calls');
});

it('fails with terminal status details when call never reaches accepted state', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['busy']
    );

    try {
        $client->callGateAndHangupWhenAccepted(45, 12345678, 10);
        $thrown = null;
    } catch (\Exception $e) {
        $thrown = $e;
    }

    expect($thrown)->toBeInstanceOf(\Exception::class);
    expect($thrown?->getMessage())->toContain('terminal status: busy');
    expect($thrown?->getMessage())->not->toContain('Bird API request failed');
    expect($client->hangupCalls)->toBe(0);
});

it('hangs up when call reaches accepted state', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['accepted']
    );

    $client->callGateAndHangupWhenAccepted(45, 12345678, 10);

    expect($client->hangupCalls)->toBe(1);
    expect($client->createPayloads)->toHaveCount(1);
    expect($client->createPayloads[0]['workspaceId'])->toBe('workspace_1');
    expect($client->createPayloads[0]['channelId'])->toBe('channel_1');
    expect($client->createPayloads[0]['payload']['from'])->toBe('+4532330288');
    expect($client->createPayloads[0]['payload']['to'])->toBe('+4512345678');
    expect($client->createPayloads[0]['payload']['ringTimeout'])->toBe(10);
    expect(array_key_exists('timeout', $client->createPayloads[0]['payload']))->toBeFalse();
    expect($client->hangupPayloads[0])->toBe([]);
});

it('hangs up immediately when status transitions to accepted', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['ringing', 'accepted', 'ongoing']
    );

    $client->callGateAndHangupWhenAccepted(45, 12345678, 10);

    expect($client->hangupCalls)->toBe(1);
    expect($client->statusesSeen)->toBe(['ringing', 'accepted']);
});

it('maps legacy timeout option to documented ringTimeout payload field', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['accepted']
    );

    $client->createOutboundTestCallAndHangupWhenAccepted('workspace_1', 'channel_1', [
        'to' => '+4512345678',
        'timeout' => 12,
    ]);

    expect($client->createPayloads)->toHaveCount(1);
    expect($client->createPayloads[0]['payload']['ringTimeout'])->toBe(12);
    expect(array_key_exists('timeout', $client->createPayloads[0]['payload']))->toBeFalse();
});

it('clamps derived ringTimeout to Bird documented max when gate timeout is high', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['accepted']
    );

    $client->callGateAndHangupWhenAccepted(45, 12345678, 1000);

    expect($client->createPayloads)->toHaveCount(1);
    expect($client->createPayloads[0]['payload']['ringTimeout'])->toBe(120);
    expect(array_key_exists('timeout', $client->createPayloads[0]['payload']))->toBeFalse();
});

it('passes documented hangup cause when provided', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['accepted']
    );

    $client->createOutboundTestCallAndHangupWhenAccepted('workspace_1', 'channel_1', [
        'to' => '+4512345678',
        'hangupCause' => 'rejected',
    ]);

    expect($client->hangupCalls)->toBe(1);
    expect($client->hangupPayloads[0]['cause'])->toBe('rejected');
});

it('drops unsupported hangup cause values from request payload', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['accepted']
    );

    $client->createOutboundTestCallAndHangupWhenAccepted('workspace_1', 'channel_1', [
        'to' => '+4512345678',
        'hangupCause' => 'completed',
    ]);

    expect($client->hangupCalls)->toBe(1);
    expect($client->hangupPayloads[0])->toBe([]);
});

it('fails fast when workspace id is missing for gate flash calls', function (): void {
    $client = new BirdGateCallClientFake(workspaceId: '', channelId: 'channel_1');

    expect(fn() => $client->callGateViaFlashCall(45, 12345678, 10))
        ->toThrow(\Exception::class, 'Bird workspaceId is not configured for gate flash calls');
});

it('creates gate flash call with documented ringTimeout payload', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1'
    );
    $client->flashCreateResponse = ['id' => 'flash_123', 'status' => 'accepted', 'from' => '+4532330288'];

    $client->callGateViaFlashCall(45, 12345678, 10);

    expect($client->flashCreatePayloads)->toHaveCount(1);
    expect($client->flashCreatePayloads[0]['workspaceId'])->toBe('workspace_1');
    expect($client->flashCreatePayloads[0]['channelId'])->toBe('channel_1');
    expect($client->flashCreatePayloads[0]['payload']['from'])->toBe('+4532330288');
    expect($client->flashCreatePayloads[0]['payload']['to'])->toBe('+4512345678');
    expect($client->flashCreatePayloads[0]['payload']['ringTimeout'])->toBe(10);
    expect(array_key_exists('timeout', $client->flashCreatePayloads[0]['payload']))->toBeFalse();
});

it('polls flash call and succeeds once accepted', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1'
    );
    $client->flashCreateResponse = ['id' => 'flash_123', 'status' => 'starting', 'from' => '+4532330288'];
    $client->flashStatusQueue = ['ringing', 'accepted', 'completed'];

    $client->callGateViaFlashCall(45, 12345678, 10);

    expect($client->flashStatusesSeen)->toBe(['ringing', 'accepted']);
});

it('fails flash gate flow when terminal failure status is returned', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1'
    );
    $client->flashCreateResponse = ['id' => 'flash_123', 'status' => 'starting', 'from' => '+4532330288'];
    $client->flashStatusQueue = ['busy'];

    expect(fn() => $client->callGateViaFlashCall(45, 12345678, 10))
        ->toThrow(\Exception::class, 'Gate flash call failed with status: busy');
});

it('falls back to regular gate call when flash caller id is not confirmed', function (): void {
    $client = new BirdGateCallClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1',
        statusQueue: ['accepted']
    );
    $client->flashCreateResponse = ['id' => 'flash_123', 'status' => 'accepted'];

    $client->callGatePreferringFlashCall(45, 12345678, 10);

    expect($client->flashCreatePayloads)->toHaveCount(1);
    expect($client->createPayloads)->toHaveCount(1);
    expect($client->createPayloads[0]['payload']['from'])->toBe('+4532330288');
});

it('does not fallback to regular gate call when flash succeeds', function (): void {
    $client = new BirdGateCallFlashFallbackClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1'
    );

    $client->callGatePreferringFlashCall(45, 12345678, 10);

    expect($client->flashCallAttempts)->toBe(1);
    expect($client->regularCallAttempts)->toBe(0);
});

it('falls back to regular gate call when flash fails', function (): void {
    $client = new BirdGateCallFlashFallbackClientFake(
        workspaceId: 'workspace_1',
        channelId: 'channel_1'
    );
    $client->shouldFlashFail = true;

    $client->callGatePreferringFlashCall(45, 12345678, 10);

    expect($client->flashCallAttempts)->toBe(1);
    expect($client->regularCallAttempts)->toBe(1);
});
