<?php

declare(strict_types=1);

namespace ApiModel\Tests\Integration\Auth;

use ApiModel\Shared\Dto\Auth\LoginInput;
use ApiModel\Service\AuthService;
use ApiModel\Infrastructure\Persistence\Repositories\PdoPasswordResetRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoPlatformAuditRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoRefreshTokenRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoUserRepository;
use ApiModel\Shared\Exceptions\BadRequestException;
use ApiModel\Shared\Exceptions\UnauthorizedException;
use PDO;
use PHPUnit\Framework\TestCase;

final class AuthFlowTest extends TestCase
{
    private PDO $pdo;
    private AuthService $auth;

    protected function setUp(): void
    {
        parent::setUp();

        $this->pdo = new PDO('sqlite::memory:');
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

        $this->createSchema();
        $this->seedInternalUser();
        $this->configureEnv();

        $this->auth = new AuthService(
            new PdoUserRepository($this->pdo),
            new PdoRefreshTokenRepository($this->pdo),
            new PdoPasswordResetRepository($this->pdo),
            new PdoPlatformAuditRepository($this->pdo),
            null,
        );
    }

    public function testLoginWithValidCredentialsReturnsAccessAndRefreshToken(): void
    {
        $result = $this->auth->login(new LoginInput('admin@example.com', 'Secret123!'));

        $this->assertArrayHasKey('access_token', $result);
        $this->assertArrayHasKey('refresh_token', $result);
        $this->assertArrayHasKey('user', $result);
        $this->assertSame('admin@example.com', $result['user']['email']);
        $this->assertSame('ADMIN', $result['user']['role']);

        $stmt = $this->pdo->query("SELECT action, actor_user_id FROM auth.logs WHERE action = 'USER_LOGIN'");
        $log = $stmt->fetch();
        $this->assertNotFalse($log);
        $this->assertSame('USER_LOGIN', $log['action']);
        $this->assertSame('u-admin-1', $log['actor_user_id']);
    }

    public function testLoginWithInvalidPasswordThrowsUnauthorized(): void
    {
        $this->expectException(UnauthorizedException::class);
        $this->expectExceptionMessage('Credenciales inválidas');
        $this->expectExceptionCode(401);

        $this->auth->login(new LoginInput('admin@example.com', 'wrong-password'));
    }

    public function testLoginWithUnknownEmailThrowsUnauthorized(): void
    {
        $this->expectException(UnauthorizedException::class);
        $this->expectExceptionMessage('Credenciales inválidas');
        $this->expectExceptionCode(401);

        $this->auth->login(new LoginInput('unknown@example.com', 'Secret123!'));
    }

    public function testRefreshWithValidTokenRevokesOldTokenAndIssuesNewTokens(): void
    {
        $session = $this->auth->login(new LoginInput('admin@example.com', 'Secret123!'));
        $refresh = $session['refresh_token'];

        $newSession = $this->auth->refresh($refresh);

        $this->assertArrayHasKey('access_token', $newSession);
        $this->assertArrayHasKey('refresh_token', $newSession);
        $this->assertNotSame($session['refresh_token'], $newSession['refresh_token']);

        $hash = hash('sha256', $refresh);
        $st = $this->pdo->prepare('SELECT revoked FROM auth.refresh_tokens WHERE token_hash = ?');
        $st->execute([$hash]);
        $oldToken = $st->fetch();

        $this->assertNotFalse($oldToken);
        $this->assertSame(1, (int) $oldToken['revoked']);
    }

    public function testRefreshWithInvalidTokenThrowsUnauthorized(): void
    {
        $this->expectException(UnauthorizedException::class);
        $this->expectExceptionMessage('Sesión expirada');
        $this->expectExceptionCode(401);

        $this->auth->refresh('invalid-refresh-token');
    }

    public function testRefreshWithExpiredTokenThrowsUnauthorized(): void
    {
        $session = $this->auth->login(new LoginInput('admin@example.com', 'Secret123!'));
        $hash = hash('sha256', $session['refresh_token']);
        $expiredAt = (new \DateTimeImmutable('-1 minute'))->format('Y-m-d H:i:s');

        $this->pdo->prepare('UPDATE auth.refresh_tokens SET expires_at = ? WHERE token_hash = ?')
            ->execute([$expiredAt, $hash]);

        $this->expectException(UnauthorizedException::class);
        $this->expectExceptionMessage('Sesión expirada');
        $this->expectExceptionCode(401);

        $this->auth->refresh($session['refresh_token']);
    }

    public function testLogoutRevokesRefreshToken(): void
    {
        $session = $this->auth->login(new LoginInput('admin@example.com', 'Secret123!'));
        $refresh = $session['refresh_token'];
        $userId = $session['user']['id'];

        $this->auth->logout($userId, $refresh);

        $hash = hash('sha256', $refresh);
        $st = $this->pdo->prepare('SELECT revoked FROM auth.refresh_tokens WHERE user_id = ? AND token_hash = ?');
        $st->execute([$userId, $hash]);
        $row = $st->fetch();

        $this->assertNotFalse($row);
        $this->assertSame(1, (int) $row['revoked']);
    }

    public function testForgotPasswordCreatesResetCodeForActiveUser(): void
    {
        $response = $this->auth->forgotPassword('admin@example.com');
        $this->assertSame(['ok' => true], $response);

        $st = $this->pdo->prepare('SELECT COUNT(*) FROM auth.password_reset_codes WHERE email = ?');
        $st->execute(['admin@example.com']);
        $count = (int) $st->fetchColumn();

        $this->assertSame(1, $count);
    }

    public function testResetPasswordWithValidCodeUpdatesPasswordAndRevokesTokens(): void
    {
        $session = $this->auth->login(new LoginInput('admin@example.com', 'Secret123!'));
        $userId = $session['user']['id'];

        $email = 'admin@example.com';
        $code = '123456';
        $secret = (string) $_ENV['PASSWORD_RESET_CODE_SECRET'];
        $hash = hash('sha256', $email . ':' . $code . ':' . $secret);
        $expiresAt = (new \DateTimeImmutable('+15 minutes'))->format('Y-m-d H:i:s');

        $this->pdo->prepare(
            'INSERT INTO auth.password_reset_codes (email, code_hash, expires_at, consumed_at) VALUES (?,?,?,NULL)',
        )->execute([$email, $hash, $expiresAt]);

        $this->auth->resetPassword($email, $code, 'NewPass456!');

        $st = $this->pdo->prepare('SELECT password_hash FROM auth.users WHERE id = ?');
        $st->execute([$userId]);
        $row = $st->fetch();

        $this->assertNotFalse($row);
        $this->assertTrue(password_verify('NewPass456!', (string) $row['password_hash']));

        $rt = $this->pdo->prepare('SELECT COUNT(*) FROM auth.refresh_tokens WHERE user_id = ? AND revoked = 0');
        $rt->execute([$userId]);
        $activeCount = (int) $rt->fetchColumn();
        $this->assertSame(0, $activeCount);
    }

    public function testResetPasswordWithInvalidCodeThrowsBadRequest(): void
    {
        $this->expectException(BadRequestException::class);
        $this->expectExceptionMessage('Código inválido o expirado');
        $this->expectExceptionCode(400);

        $this->auth->resetPassword('admin@example.com', '000000', 'NewPass456!');
    }

    private function createSchema(): void
    {
        $this->pdo->exec("ATTACH DATABASE ':memory:' AS auth");

        $this->pdo->exec(
            'CREATE TABLE auth.roles (
                id TEXT PRIMARY KEY,
                name_es TEXT NOT NULL UNIQUE,
                name_en TEXT NOT NULL UNIQUE,
                description_es TEXT,
                description_en TEXT,
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
            )',
        );

        $this->pdo->exec(
            'CREATE TABLE auth.users (
                id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT NOT NULL UNIQUE,
                password_hash TEXT NULL,
                external_id TEXT NULL,
                language TEXT NOT NULL DEFAULT "es",
                is_active INTEGER NOT NULL DEFAULT 1,
                updated_at TEXT NULL
            )',
        );

        $this->pdo->exec(
            'CREATE TABLE auth.role_user (
                role_id TEXT NOT NULL,
                user_id TEXT NOT NULL,
                PRIMARY KEY (role_id, user_id)
            )',
        );

        $this->pdo->exec(
            'CREATE TABLE auth.refresh_tokens (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id TEXT NOT NULL,
                token_hash TEXT NOT NULL,
                expires_at TEXT NOT NULL,
                revoked INTEGER NOT NULL DEFAULT 0
            )',
        );

        $this->pdo->exec(
            'CREATE TABLE auth.password_reset_codes (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                email TEXT NOT NULL,
                code_hash TEXT NOT NULL,
                expires_at TEXT NOT NULL,
                consumed_at TEXT NULL
            )',
        );

        $this->pdo->exec(
            'CREATE TABLE auth.logs (
                id TEXT PRIMARY KEY,
                action TEXT NOT NULL,
                actor_user_id TEXT NULL,
                target_id TEXT NULL,
                metadata TEXT NULL,
                ip_address TEXT NULL,
                user_agent TEXT NULL,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )',
        );
    }

    private function seedInternalUser(): void
    {
        $hash = password_hash('Secret123!', PASSWORD_BCRYPT, ['cost' => 10]);
        $this->pdo->prepare(
            'INSERT INTO auth.users (id, name, email, password_hash, language, is_active)
             VALUES (?,?,?,?,?,1)',
        )->execute(['u-admin-1', 'Admin', 'admin@example.com', $hash, 'es']);

        $this->pdo->prepare(
            'INSERT INTO auth.roles (id, name_es, name_en, description_es, description_en, is_active)
             VALUES (?,?,?,?,?,1)',
        )->execute(['r-admin-1', 'ADMIN', 'ADMIN', 'Admin', 'Admin']);

        $this->pdo->prepare(
            'INSERT INTO auth.role_user (role_id, user_id) VALUES (?,?)',
        )->execute(['r-admin-1', 'u-admin-1']);
    }

    private function configureEnv(): void
    {
        $_ENV['JWT_SECRET'] = 'test-jwt-secret';
        $_ENV['JWT_EXPIRES_IN'] = '15m';
        $_ENV['JWT_REFRESH_EXPIRES_DAYS'] = '7';
        $_ENV['JWT_KID'] = '';
        $_ENV['JWT_SECRET_PREVIOUS'] = '';
        $_ENV['EXTERNAL_APP_MOCK'] = 'false';
        $_ENV['EXTERNAL_APP_API_URL'] = '';
        $_ENV['PASSWORD_RESET_CODE_SECRET'] = 'test-reset-secret';
        $_ENV['PASSWORD_RESET_CODE_TTL_MIN'] = '15';
    }
}


