<?php

namespace MintyPHP\Tests\Service\Auth;

use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Auth\AuthService;
use MintyPHP\Service\Auth\AuthSessionTenantContextService;
use MintyPHP\Service\Auth\EmailVerificationService;
use MintyPHP\Service\Auth\RememberMeService;
use MintyPHP\Service\Auth\TenantSsoService;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserTenantContextService;
use PHPUnit\Framework\TestCase;

class AuthServiceTest extends TestCase
{
    // ---------------------------------------------------------------
    // canLoginToTenant
    // ---------------------------------------------------------------

    public function testCanLoginToTenantReturnsFalseForInvalidUserId(): void
    {
        $service = $this->newService();
        $this->assertFalse($service->canLoginToTenant(0, 5));
    }

    public function testCanLoginToTenantReturnsFalseForInvalidTenantId(): void
    {
        $service = $this->newService();
        $this->assertFalse($service->canLoginToTenant(1, 0));
    }

    public function testCanLoginToTenantReturnsTrueWhenTenantIsAssigned(): void
    {
        $tenantCtx = $this->createMock(UserTenantContextService::class);
        $tenantCtx->method('getAvailableTenants')->willReturn([
            ['id' => 3],
            ['id' => 7],
        ]);

        $service = $this->newService(userTenantContextService: $tenantCtx);
        $this->assertTrue($service->canLoginToTenant(1, 7));
    }

    public function testCanLoginToTenantReturnsFalseWhenTenantNotAssigned(): void
    {
        $tenantCtx = $this->createMock(UserTenantContextService::class);
        $tenantCtx->method('getAvailableTenants')->willReturn([
            ['id' => 3],
        ]);

        $service = $this->newService(userTenantContextService: $tenantCtx);
        $this->assertFalse($service->canLoginToTenant(1, 99));
    }

    // ---------------------------------------------------------------
    // refreshSessionAuthState
    // ---------------------------------------------------------------

    public function testRefreshSessionRequiresLogoutWhenUserIdIsZero(): void
    {
        $service = $this->newService();
        $result = $service->refreshSessionAuthState(0);

        $this->assertFalse($result['ok']);
        $this->assertTrue($result['logout_required']);
        $this->assertSame('user_missing', $result['reason']);
    }

    public function testRefreshSessionRequiresLogoutWhenUserNotFound(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('findAuthzSnapshot')->willReturn(null);

        $service = $this->newService(userReadRepository: $userRead);
        $result = $service->refreshSessionAuthState(5);

        $this->assertFalse($result['ok']);
        $this->assertTrue($result['logout_required']);
        $this->assertSame('user_not_found', $result['reason']);
    }

    public function testRefreshSessionRequiresLogoutWhenUserIsInactive(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('findAuthzSnapshot')->willReturn(['active' => 0, 'authz_version' => 1]);

        $service = $this->newService(userReadRepository: $userRead);
        $result = $service->refreshSessionAuthState(5);

        $this->assertFalse($result['ok']);
        $this->assertTrue($result['logout_required']);
        $this->assertSame('inactive', $result['reason']);
    }

    public function testRefreshSessionReturnsOkWhenVersionMatchesAndTenantActive(): void
    {
        $session = $this->createMock(SessionStoreInterface::class);
        $session->method('get')->willReturnMap([
            ['user', [], ['id' => 5, 'authz_version' => 2]],
            ['current_tenant', [], ['id' => 10]],
            ['no_active_tenant', false, false],
        ]);

        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('findAuthzSnapshot')->willReturn(['active' => 1, 'authz_version' => 2]);

        $service = $this->newService(userReadRepository: $userRead, sessionStore: $session);
        $result = $service->refreshSessionAuthState(5);

        $this->assertTrue($result['ok']);
        $this->assertFalse($result['logout_required']);
        $this->assertFalse($result['refreshed']);
    }

    public function testRefreshSessionRefreshesWhenVersionMismatch(): void
    {
        $session = $this->createMock(SessionStoreInterface::class);
        $session->method('get')->willReturnCallback(function (string $key, mixed $default) {
            if ($key === 'user') {
                return ['id' => 5, 'authz_version' => 1];
            }
            if ($key === 'no_active_tenant') {
                return false;
            }
            return $default;
        });
        $session->expects($this->atLeastOnce())->method('set');

        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('findAuthzSnapshot')->willReturn(['active' => 1, 'authz_version' => 3]);
        $userRead->method('find')->willReturn(['id' => 5, 'active' => 1, 'authz_version' => 3]);

        $permService = $this->createMock(PermissionService::class);
        $permService->expects($this->once())->method('getUserPermissions')->with(5, true);

        $tenantCtxService = $this->createMock(AuthSessionTenantContextService::class);
        $tenantCtxService->expects($this->once())->method('hydrateForUser')->with(5);

        $service = $this->newService(
            userReadRepository: $userRead,
            permissionService: $permService,
            authSessionTenantContextService: $tenantCtxService,
            sessionStore: $session
        );
        $result = $service->refreshSessionAuthState(5);

        $this->assertTrue($result['ok']);
        $this->assertFalse($result['logout_required']);
        $this->assertTrue($result['refreshed']);
    }

    public function testRefreshSessionRequiresLogoutWhenNoActiveTenantAfterRefresh(): void
    {
        $session = $this->createMock(SessionStoreInterface::class);
        $session->method('get')->willReturnCallback(function (string $key, mixed $default) {
            if ($key === 'user') {
                return ['id' => 5, 'authz_version' => 1];
            }
            if ($key === 'no_active_tenant') {
                return true; // no active tenant
            }
            return $default;
        });

        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('findAuthzSnapshot')->willReturn(['active' => 1, 'authz_version' => 2]);
        $userRead->method('find')->willReturn(['id' => 5, 'active' => 1, 'authz_version' => 2]);

        $service = $this->newService(userReadRepository: $userRead, sessionStore: $session);
        $result = $service->refreshSessionAuthState(5);

        $this->assertFalse($result['ok']);
        $this->assertTrue($result['logout_required']);
        $this->assertSame('no_active_tenant', $result['reason']);
    }

    // ---------------------------------------------------------------
    // loginUserById
    // ---------------------------------------------------------------

    public function testLoginUserByIdReturnsErrorForZeroId(): void
    {
        $service = $this->newService();
        $result = $service->loginUserById(0);

        $this->assertFalse($result['ok']);
        $this->assertSame('user_not_found', $result['error']);
    }

    public function testLoginUserByIdReturnsErrorWhenUserNotFound(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(null);

        $service = $this->newService(userReadRepository: $userRead);
        $result = $service->loginUserById(5);

        $this->assertFalse($result['ok']);
        $this->assertSame('user_not_found', $result['error']);
    }

    public function testLoginUserByIdReturnsErrorWhenUserInactive(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(['id' => 5, 'active' => 0]);

        $service = $this->newService(userReadRepository: $userRead);
        $result = $service->loginUserById(5);

        $this->assertFalse($result['ok']);
        $this->assertSame('user_inactive', $result['error']);
    }

    public function testLoginUserByIdSucceedsWithActiveTenant(): void
    {
        $_SESSION = [];

        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(['id' => 5, 'active' => 1]);

        $userWrite = $this->createMock(UserWriteRepositoryInterface::class);
        $userWrite->expects($this->once())->method('updateLastLogin')->with(5, 'microsoft');

        $session = $this->createMock(SessionStoreInterface::class);
        $session->expects($this->atLeastOnce())->method('set');
        $session->method('get')->willReturnMap([
            ['user', [], ['id' => 5, 'active' => 1]],
            ['current_tenant', [], ['id' => 10]],
            ['no_active_tenant', false, false],
        ]);

        $permService = $this->createMock(PermissionService::class);
        $permService->expects($this->once())->method('getUserPermissions')->with(5, true);

        $tenantCtxService = $this->createMock(AuthSessionTenantContextService::class);
        $tenantCtxService->expects($this->once())->method('hydrateForUser')->with(5);

        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())->method('record')
            ->with('auth.login.success', 'success', $this->anything());

        $service = $this->newService(
            userReadRepository: $userRead,
            userWriteRepository: $userWrite,
            permissionService: $permService,
            authSessionTenantContextService: $tenantCtxService,
            systemAuditService: $audit,
            sessionStore: $session
        );
        $result = $service->loginUserById(5, null, 'microsoft');

        $this->assertTrue($result['ok']);
        $this->assertSame(5, (int) ($result['user']['id'] ?? 0));
    }

    public function testLoginUserByIdFailsWhenNoActiveTenant(): void
    {
        $_SESSION = [];

        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(['id' => 5, 'active' => 1]);

        $session = $this->createMock(SessionStoreInterface::class);
        $session->method('get')->willReturnCallback(function (string $key, mixed $default) {
            if ($key === 'user') {
                return ['id' => 5, 'active' => 1];
            }
            if ($key === 'no_active_tenant') {
                return true;
            }
            return $default;
        });

        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())->method('record')
            ->with('auth.login.failed', 'failed', $this->anything());

        $service = $this->newService(
            userReadRepository: $userRead,
            systemAuditService: $audit,
            sessionStore: $session
        );
        $result = $service->loginUserById(5);

        $this->assertFalse($result['ok']);
        $this->assertSame('login_no_active_tenant', $result['error']);
    }

    public function testLoginUserByIdSetsPreferredTenantWhenProvided(): void
    {
        $_SESSION = [];

        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(['id' => 5, 'active' => 1]);

        $tenantCtx = $this->createMock(UserTenantContextService::class);
        $tenantCtx->expects($this->once())
            ->method('setCurrentTenant')
            ->with(5, 42)
            ->willReturn(['ok' => true]);

        $session = $this->createMock(SessionStoreInterface::class);
        $session->method('get')->willReturnMap([
            ['user', [], ['id' => 5, 'active' => 1]],
            ['current_tenant', [], ['id' => 42]],
            ['no_active_tenant', false, false],
        ]);
        $session->expects($this->atLeastOnce())->method('set');

        $service = $this->newService(
            userReadRepository: $userRead,
            userTenantContextService: $tenantCtx,
            sessionStore: $session
        );
        $result = $service->loginUserById(5, 42, 'local');

        $this->assertTrue($result['ok']);
    }

    // ---------------------------------------------------------------
    // login — email not verified branch
    // ---------------------------------------------------------------

    public function testLoginReturnsNeedsVerificationWhenEmailNotVerified(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('findByEmail')->willReturn([
            'id' => 5,
            'email' => 'test@example.com',
            'email_verified_at' => null,
        ]);

        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())->method('record')
            ->with('auth.login.failed', 'failed', $this->callback(function (array $ctx): bool {
                return ($ctx['error_code'] ?? '') === 'email_not_verified';
            }));

        $service = $this->newService(userReadRepository: $userRead, systemAuditService: $audit);
        $result = $service->login('test@example.com', 'password');

        $this->assertFalse($result['ok']);
        $this->assertTrue($result['needs_verification'] ?? false);
        $this->assertSame('login_not_verified', $result['flash_key']);
    }

    // ---------------------------------------------------------------
    // logout behavior
    // ---------------------------------------------------------------

    public function testLogoutDueToSessionTimeoutKeepsRememberTokenAndClearsSessionTimers(): void
    {
        $_SESSION = [];
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        $removedKeys = [];
        $session = $this->createMock(SessionStoreInterface::class);
        $session->method('get')->willReturnMap([
            ['user', [], ['id' => 5]],
            ['current_tenant', [], ['id' => 9]],
        ]);
        $session->method('remove')->willReturnCallback(function (string $key) use (&$removedKeys): void {
            $removedKeys[] = $key;
        });

        $rememberMeService = $this->createMock(RememberMeService::class);
        $rememberMeService->expects($this->never())->method('forgetCurrentUser');

        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())->method('record')
            ->with('auth.session.timeout', 'success', $this->callback(function (array $context): bool {
                return (int) ($context['actor_user_id'] ?? 0) === 5
                    && (int) ($context['actor_tenant_id'] ?? 0) === 9;
            }));

        $service = $this->newService(
            rememberMeService: $rememberMeService,
            systemAuditService: $audit,
            sessionStore: $session
        );

        $service->logoutDueToSessionTimeout();
        sort($removedKeys);
        $this->assertSame(['session_last_activity', 'session_started_at'], $removedKeys);
    }

    public function testLogoutStillForgetsRememberTokenOnManualLogout(): void
    {
        $_SESSION = [];
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }

        $rememberMeService = $this->createMock(RememberMeService::class);
        $rememberMeService->expects($this->once())->method('forgetCurrentUser');

        $session = $this->createMock(SessionStoreInterface::class);
        $session->method('get')->willReturnMap([
            ['user', [], ['id' => 5]],
            ['current_tenant', [], ['id' => 11]],
        ]);

        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())->method('record')
            ->with('auth.logout', 'success', $this->callback(function (array $context): bool {
                return (int) ($context['actor_user_id'] ?? 0) === 5
                    && (int) ($context['actor_tenant_id'] ?? 0) === 11;
            }));

        $service = $this->newService(
            rememberMeService: $rememberMeService,
            systemAuditService: $audit,
            sessionStore: $session
        );

        $service->logout();
    }

    // ---------------------------------------------------------------
    // Helper
    // ---------------------------------------------------------------

    private function newService(
        ?UserReadRepositoryInterface $userReadRepository = null,
        ?UserWriteRepositoryInterface $userWriteRepository = null,
        ?UserAccountService $userAccountService = null,
        ?UserTenantContextService $userTenantContextService = null,
        ?RememberMeService $rememberMeService = null,
        ?EmailVerificationService $emailVerificationService = null,
        ?PermissionService $permissionService = null,
        ?TenantSsoService $tenantSsoService = null,
        ?AuthSessionTenantContextService $authSessionTenantContextService = null,
        ?SystemAuditService $systemAuditService = null,
        ?SessionStoreInterface $sessionStore = null
    ): AuthService {
        return new AuthService(
            $userReadRepository ?? $this->createMock(UserReadRepositoryInterface::class),
            $userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class),
            $userAccountService ?? $this->createMock(UserAccountService::class),
            $userTenantContextService ?? $this->createMock(UserTenantContextService::class),
            $rememberMeService ?? $this->createMock(RememberMeService::class),
            $emailVerificationService ?? $this->createMock(EmailVerificationService::class),
            $permissionService ?? $this->createMock(PermissionService::class),
            $tenantSsoService ?? $this->createMock(TenantSsoService::class),
            $authSessionTenantContextService ?? $this->createMock(AuthSessionTenantContextService::class),
            $systemAuditService ?? $this->createMock(SystemAuditService::class),
            $sessionStore ?? $this->createMock(SessionStoreInterface::class)
        );
    }
}
