<?php

declare(strict_types=1);

namespace Tests;

use PDO;
use PDOException;
use PHPUnit\Framework\TestCase;
use Tests\Support\HttpClient;
use Tests\Support\HttpResponse;
use Tests\Support\TestDataFactory;

/**
 * Base Test Case — Auth Service
 *
 * Lifecycle:
 *  setUpBeforeClass()   → opens HTTP client + direct PDO connection to emmanylcode_auth
 *  tearDownAfterClass() → purges ALL test records matching the sms.testdomain.local pattern
 *
 * Provides shared helpers: registerUser(), loginAs(), registerAndLogin().
 */
abstract class AuthTestCase extends TestCase
{
    protected static HttpClient $http;
    protected static PDO        $db;

    // ─── Lifecycle ────────────────────────────────────────────────────────────

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();

        static::$http = new HttpClient($_ENV['AUTH_BASE_URL'] ?? null);

        static::$db = new PDO(
            sprintf(
                'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
                $_ENV['DB_HOST'] ?? 'localhost',
                (int) ($_ENV['DB_PORT'] ?? 3306),
                $_ENV['DB_NAME'] ?? 'emmanylcode_auth'
            ),
            $_ENV['DB_USER'] ?? 'root',
            $_ENV['DB_PASS'] ?? '',
            [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            ]
        );
    }

    /**
     * TEARDOWN: Purge all test records from emmanylcode_auth.
     *
     * Targets the email pattern '%@sms.testdomain.local', which is NEVER
     * used in production. Runs DELETE against both 'auth_users' (canonical
     * schema) and 'users' (current controller table) to handle the known
     * schema/controller discrepancy.
     */
    public static function tearDownAfterClass(): void
    {
        $pattern = TestDataFactory::teardownEmailPattern(); // '%@sms.testdomain.local'

        foreach (['auth_users', 'users'] as $table) {
            try {
                $stmt = static::$db->prepare(
                    "DELETE FROM `{$table}` WHERE email LIKE :pattern"
                );
                $stmt->execute(['pattern' => $pattern]);
            } catch (PDOException) {
                // Table may not exist — safe to ignore
            }
        }

        parent::tearDownAfterClass();
    }

    // ─── Shared Helpers ───────────────────────────────────────────────────────

    /**
     * POST /api/auth/register with the given payload.
     */
    protected function registerUser(array $payload): HttpResponse
    {
        return static::$http->post('/api/auth/register', $payload);
    }

    /**
     * POST /api/auth/login with email + password.
     */
    protected function loginAs(string $email, string $password): HttpResponse
    {
        return static::$http->post('/api/auth/login', [
            'email'    => $email,
            'password' => $password,
        ]);
    }

    /**
     * Register a fresh dynamic user then immediately log them in.
     * Returns both the user payload and the login response so callers can
     * extract tokens without duplicating the registration step.
     *
     * @return array{payload: array, loginResponse: HttpResponse}
     */
    protected function registerAndLogin(array $overrides = []): array
    {
        $payload  = TestDataFactory::makeUser($overrides);
        $register = $this->registerUser($payload);

        self::assertTrue(
            $register->isCreated() || $register->isOk(),
            "registerAndLogin() helper: registration failed — " . $register->dump()
        );

        $login = $this->loginAs($payload['email'], $payload['password']);

        self::assertTrue(
            $login->isOk(),
            "registerAndLogin() helper: login failed — " . $login->dump()
        );

        return ['payload' => $payload, 'loginResponse' => $login];
    }

    /**
     * Extract the access token from a login response.
     * Handles both 'token' (current) and 'access_token' (planned) keys.
     */
    protected function extractToken(HttpResponse $loginResponse): string
    {
        $token = $loginResponse->get('token')
            ?? $loginResponse->get('access_token')
            ?? $loginResponse->dot('tokens.access_token');

        self::assertNotNull(
            $token,
            "No token found in login response. Keys: " . implode(', ', array_keys($loginResponse->data))
        );

        return (string) $token;
    }

    /**
     * Extract the refresh token — expected under 'refresh_token' or nested 'tokens.refresh_token'.
     */
    protected function extractRefreshToken(HttpResponse $loginResponse): ?string
    {
        return $loginResponse->get('refresh_token')
            ?? $loginResponse->dot('tokens.refresh_token');
    }

    // ─── Custom Assertion Helpers ─────────────────────────────────────────────

    protected function assertHasKeys(array $keys, array $data, string $message = ''): void
    {
        foreach ($keys as $key) {
            $this->assertArrayHasKey($key, $data, $message ?: "Expected key '{$key}' missing from response.");
        }
    }
}
