<?php

declare(strict_types=1);

namespace App\Tests\Unit\ApiClient;

use App\ApiClient\ApiAuthException;
use App\ApiClient\ApiClient;
use App\ApiClient\ApiHealth;
use App\ApiClient\ApiNotFoundException;
use App\ApiClient\ApiServerException;
use App\ApiClient\ApiUnreachableException;
use App\ApiClient\ApiValidationException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;

/**
 * Verifies the ApiClient's status-code → exception mapping and the
 * single-retry-on-5xx-or-connect-error policy.
 */
final class ApiClientTest extends TestCase
{
    public function testSuccessfulResponseReturnsDecodedJson(): void
    {
        $client = $this->build([new Response(200, [], (string) json_encode(['user_id' => 42, 'role' => 'admin']))]);

        $body = $client->request('GET', '/api/v1/admin/me', [], 42);

        self::assertSame(42, $body['user_id']);
        self::assertSame('admin', $body['role']);
    }

    public function test401MapsToApiAuthException(): void
    {
        $client = $this->build([new Response(401, [], (string) json_encode(['error' => 'unauthorized']))]);

        $this->expectException(ApiAuthException::class);
        $client->request('GET', '/api/v1/admin/me');
    }

    public function test403MapsToApiAuthException(): void
    {
        $client = $this->build([new Response(403, [], (string) json_encode(['error' => 'forbidden']))]);

        $this->expectException(ApiAuthException::class);
        $client->request('GET', '/api/v1/admin/policies');
    }

    public function test404MapsToApiNotFoundException(): void
    {
        $client = $this->build([new Response(404, [], (string) json_encode(['error' => 'not_found']))]);

        $this->expectException(ApiNotFoundException::class);
        $client->request('GET', '/api/v1/admin/ips/missing');
    }

    public function test400ValidationCarriesDetails(): void
    {
        $client = $this->build([new Response(400, [], (string) json_encode([
            'error' => 'validation_failed',
            'details' => ['name' => 'required'],
        ]))]);

        try {
            $client->request('POST', '/api/v1/admin/policies');
            self::fail('expected ApiValidationException');
        } catch (ApiValidationException $e) {
            self::assertSame(400, $e->statusCode);
            self::assertSame(['name' => 'required'], $e->details);
        }
    }

    public function test5xxRetriesOnceThenThrowsServerException(): void
    {
        $client = $this->build([
            new Response(500, [], (string) json_encode(['error' => 'boom'])),
            new Response(500, [], (string) json_encode(['error' => 'boom'])),
        ]);

        $this->expectException(ApiServerException::class);
        $client->request('GET', '/api/v1/admin/me');
    }

    public function test5xxRetrySucceedsOnSecondAttempt(): void
    {
        $client = $this->build([
            new Response(503, [], ''),
            new Response(200, [], (string) json_encode(['ok' => true])),
        ]);

        $body = $client->request('GET', '/api/v1/admin/me');
        self::assertSame(true, $body['ok']);
    }

    public function testConnectionErrorThrowsApiUnreachable(): void
    {
        $client = $this->build([
            new ConnectException('connection refused', new Request('GET', '/')),
            new ConnectException('connection refused', new Request('GET', '/')),
        ]);

        $this->expectException(ApiUnreachableException::class);
        $client->request('GET', '/api/v1/admin/me');
    }

    public function testHealthMarkedReachableAfterSuccess(): void
    {
        $health = new ApiHealth();
        $client = $this->build([new Response(200, [], '{}')], $health);

        $client->request('GET', '/healthz');

        self::assertTrue($health->isReachable());
        self::assertNotNull($health->lastSuccessAt());
    }

    public function testHealthMarkedUnreachableAfterConnectionError(): void
    {
        $health = new ApiHealth();
        $client = $this->build([
            new ConnectException('refused', new Request('GET', '/')),
            new ConnectException('refused', new Request('GET', '/')),
        ], $health);

        try {
            $client->request('GET', '/healthz');
        } catch (ApiUnreachableException) {
            // expected
        }

        self::assertFalse($health->isReachable());
    }

    public function testServiceTokenAttachedAsBearer(): void
    {
        $captured = [];
        $mock = new MockHandler([new Response(200, [], '{}')]);
        $stack = HandlerStack::create($mock);
        $stack->push(\GuzzleHttp\Middleware::tap(static function ($req) use (&$captured): void {
            $captured[] = $req->getHeaderLine('Authorization');
        }));
        $http = new Client(['handler' => $stack]);
        $client = new ApiClient($http, 'irdb_svc_TESTTOKEN', new ApiHealth());

        $client->request('GET', '/api/v1/admin/me', [], 42);

        self::assertSame('Bearer irdb_svc_TESTTOKEN', $captured[0]);
    }

    public function testActingUserIdAttachedWhenSet(): void
    {
        $captured = [];
        $mock = new MockHandler([new Response(200, [], '{}')]);
        $stack = HandlerStack::create($mock);
        $stack->push(\GuzzleHttp\Middleware::tap(static function ($req) use (&$captured): void {
            $captured[] = $req->getHeaderLine('X-Acting-User-Id');
        }));
        $http = new Client(['handler' => $stack]);
        $client = new ApiClient($http, 'tok', new ApiHealth());

        $client->request('GET', '/api/v1/admin/me', [], 42);

        self::assertSame('42', $captured[0]);
    }

    public function testActingUserIdOmittedWhenNull(): void
    {
        $captured = [];
        $mock = new MockHandler([new Response(200, [], '{}')]);
        $stack = HandlerStack::create($mock);
        $stack->push(\GuzzleHttp\Middleware::tap(static function ($req) use (&$captured): void {
            $captured[] = $req->hasHeader('X-Acting-User-Id');
        }));
        $http = new Client(['handler' => $stack]);
        $client = new ApiClient($http, 'tok', new ApiHealth());

        $client->request('POST', '/api/v1/auth/users/upsert-local');

        self::assertFalse($captured[0]);
    }

    /**
     * @param list<\Psr\Http\Message\ResponseInterface|\Throwable> $responses
     */
    private function build(array $responses, ?ApiHealth $health = null): ApiClient
    {
        $mock = new MockHandler($responses);
        $stack = HandlerStack::create($mock);
        $http = new Client(['handler' => $stack]);

        return new ApiClient($http, 'tok', $health ?? new ApiHealth());
    }
}
