<?php

namespace MintyPHP\Tests\Service\User;

use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Tenant\TenantScopeService;
use MintyPHP\Service\User\UserDirectoryGateway;
use MintyPHP\Service\User\UserTenantContextService;
use PHPUnit\Framework\TestCase;

final class UserTenantContextServiceTest extends TestCase
{
    public function testGetCurrentTenantIdReturnsNullWhenUserMissing(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(null);

        $service = $this->makeService(userRead: $userRead);

        $this->assertNull($service->getCurrentTenantId(42));
    }

    public function testGetCurrentTenantIdReturnsNullWhenUserHasNoAccessibleTenants(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(['current_tenant_id' => 5, 'primary_tenant_id' => 5]);

        $scope = $this->createMock(TenantScopeService::class);
        $scope->method('getUserTenantIds')->willReturn([]);

        $service = $this->makeService(userRead: $userRead, scope: $scope);

        $this->assertNull($service->getCurrentTenantId(42));
    }

    public function testGetCurrentTenantIdPrefersCurrentWhenInAccessibleSet(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn([
            'current_tenant_id' => 7,
            'primary_tenant_id' => 3,
        ]);

        $scope = $this->createMock(TenantScopeService::class);
        $scope->method('getUserTenantIds')->willReturn([3, 7, 9]);

        $service = $this->makeService(userRead: $userRead, scope: $scope);

        $this->assertSame(7, $service->getCurrentTenantId(42));
    }

    public function testGetCurrentTenantIdFallsBackToPrimaryWhenCurrentNotAccessible(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn([
            'current_tenant_id' => 999,
            'primary_tenant_id' => 3,
        ]);

        $scope = $this->createMock(TenantScopeService::class);
        $scope->method('getUserTenantIds')->willReturn([3, 9]);

        $service = $this->makeService(userRead: $userRead, scope: $scope);

        $this->assertSame(3, $service->getCurrentTenantId(42));
    }

    public function testGetCurrentTenantIdFallsBackToFirstAccessibleWhenNeitherCurrentNorPrimaryMatch(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn([
            'current_tenant_id' => 0,
            'primary_tenant_id' => 999,
        ]);

        $scope = $this->createMock(TenantScopeService::class);
        $scope->method('getUserTenantIds')->willReturn([9, 11]);

        $service = $this->makeService(userRead: $userRead, scope: $scope);

        $this->assertSame(9, $service->getCurrentTenantId(42));
    }

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

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->expects($this->never())->method('findTenant');

        $service = $this->makeService(userRead: $userRead, directory: $directory);

        $this->assertNull($service->getCurrentTenant(42));
    }

    public function testGetCurrentTenantLoadsTenantRecord(): void
    {
        $userRead = $this->createMock(UserReadRepositoryInterface::class);
        $userRead->method('find')->willReturn(['current_tenant_id' => 7, 'primary_tenant_id' => 7]);

        $scope = $this->createMock(TenantScopeService::class);
        $scope->method('getUserTenantIds')->willReturn([7]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->expects($this->once())
            ->method('findTenant')
            ->with(7)
            ->willReturn(['id' => 7, 'description' => 'Acme']);

        $service = $this->makeService(userRead: $userRead, scope: $scope, directory: $directory);

        $this->assertSame(['id' => 7, 'description' => 'Acme'], $service->getCurrentTenant(42));
    }

    public function testGetAvailableTenantsReturnsEmptyWhenNoneAccessible(): void
    {
        $scope = $this->createMock(TenantScopeService::class);
        $scope->method('getUserTenantIds')->willReturn([]);

        $service = $this->makeService(scope: $scope);

        $this->assertSame([], $service->getAvailableTenants(42));
    }

    public function testGetAvailableTenantsFiltersInactiveAndSortsByDescription(): void
    {
        $scope = $this->createMock(TenantScopeService::class);
        $scope->method('getUserTenantIds')->willReturn([1, 2, 3]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->method('findTenant')->willReturnCallback(static function (int $id): ?array {
            return match ($id) {
                1 => ['id' => 1, 'description' => 'Charlie', 'status' => 'active'],
                2 => ['id' => 2, 'description' => 'Alpha', 'status' => 'inactive'],
                3 => ['id' => 3, 'description' => 'Bravo', 'status' => 'active'],
                default => null,
            };
        });

        $service = $this->makeService(scope: $scope, directory: $directory);
        $result = $service->getAvailableTenants(42);

        $this->assertCount(2, $result);
        $this->assertSame('Bravo', $result[0]['description']);
        $this->assertSame('Charlie', $result[1]['description']);
    }

    public function testGetAssignedActiveTenantsReturnsEmptyForInvalidUserId(): void
    {
        $service = $this->makeService();

        $this->assertSame([], $service->getAssignedActiveTenants(0));
        $this->assertSame([], $service->getAssignedActiveTenants(-1));
    }

    public function testGetAssignedActiveTenantsFiltersInactiveAndPreservesAssignedOrder(): void
    {
        $userTenant = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenant->method('listTenantIdsByUserId')->willReturn([3, 1, 2, 3, 0]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->expects($this->once())
            ->method('listActiveTenantIdsByIds')
            ->with([3, 1, 2])
            ->willReturn([1, 3]);
        $directory->expects($this->once())
            ->method('listTenantsByIds')
            ->with([1, 3])
            ->willReturn([
                ['id' => 3, 'description' => 'Zulu'],
                ['id' => 1, 'description' => 'Alpha'],
            ]);

        $service = $this->makeService(userTenant: $userTenant, directory: $directory);
        $result = $service->getAssignedActiveTenants(42);

        $this->assertCount(2, $result);
        $this->assertSame(1, $result[0]['id']);
        $this->assertSame(3, $result[1]['id']);
    }

    public function testGetAssignedActiveTenantsReturnsEmptyWhenNoneActive(): void
    {
        $userTenant = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenant->method('listTenantIdsByUserId')->willReturn([1, 2]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->method('listActiveTenantIdsByIds')->willReturn([]);
        $directory->expects($this->never())->method('listTenantsByIds');

        $service = $this->makeService(userTenant: $userTenant, directory: $directory);

        $this->assertSame([], $service->getAssignedActiveTenants(42));
    }

    public function testSetCurrentTenantRejectsInvalidTenantId(): void
    {
        $service = $this->makeService();

        $this->assertSame(['ok' => false, 'error' => 'invalid_tenant'], $service->setCurrentTenant(42, 0));
        $this->assertSame(['ok' => false, 'error' => 'invalid_tenant'], $service->setCurrentTenant(42, -1));
    }

    public function testSetCurrentTenantRejectsUnassignedTenant(): void
    {
        $userTenant = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenant->method('listTenantIdsByUserId')->willReturn([1, 2]);

        $service = $this->makeService(userTenant: $userTenant);

        $this->assertSame(['ok' => false, 'error' => 'tenant_not_assigned'], $service->setCurrentTenant(42, 999));
    }

    public function testSetCurrentTenantRejectsMissingTenant(): void
    {
        $userTenant = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenant->method('listTenantIdsByUserId')->willReturn([7]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->method('findTenant')->willReturn(null);

        $service = $this->makeService(userTenant: $userTenant, directory: $directory);

        $this->assertSame(['ok' => false, 'error' => 'tenant_not_found'], $service->setCurrentTenant(42, 7));
    }

    public function testSetCurrentTenantRejectsInactiveTenant(): void
    {
        $userTenant = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenant->method('listTenantIdsByUserId')->willReturn([7]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->method('findTenant')->willReturn(['id' => 7, 'status' => 'inactive']);

        $service = $this->makeService(userTenant: $userTenant, directory: $directory);

        $this->assertSame(['ok' => false, 'error' => 'tenant_inactive'], $service->setCurrentTenant(42, 7));
    }

    public function testSetCurrentTenantReturnsUpdateFailedWhenPersistFails(): void
    {
        $userTenant = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenant->method('listTenantIdsByUserId')->willReturn([7]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $directory->method('findTenant')->willReturn(['id' => 7, 'status' => 'active']);

        $userWrite = $this->createMock(UserWriteRepositoryInterface::class);
        $userWrite->method('setCurrentTenant')->willReturn(false);

        $service = $this->makeService(userTenant: $userTenant, userWrite: $userWrite, directory: $directory);

        $this->assertSame(['ok' => false, 'error' => 'update_failed'], $service->setCurrentTenant(42, 7));
    }

    public function testSetCurrentTenantPersistsAndReturnsTenantOnSuccess(): void
    {
        $userTenant = $this->createMock(UserTenantRepositoryInterface::class);
        $userTenant->method('listTenantIdsByUserId')->willReturn([7]);

        $directory = $this->createMock(UserDirectoryGateway::class);
        $tenant = ['id' => 7, 'status' => 'active', 'description' => 'Acme'];
        $directory->method('findTenant')->willReturn($tenant);

        $userWrite = $this->createMock(UserWriteRepositoryInterface::class);
        $userWrite->expects($this->once())
            ->method('setCurrentTenant')
            ->with(42, 7)
            ->willReturn(true);

        $service = $this->makeService(userTenant: $userTenant, userWrite: $userWrite, directory: $directory);

        $this->assertSame(['ok' => true, 'tenant' => $tenant], $service->setCurrentTenant(42, 7));
    }

    private function makeService(
        ?UserReadRepositoryInterface $userRead = null,
        ?UserWriteRepositoryInterface $userWrite = null,
        ?UserTenantRepositoryInterface $userTenant = null,
        ?TenantScopeService $scope = null,
        ?UserDirectoryGateway $directory = null,
    ): UserTenantContextService {
        return new UserTenantContextService(
            $userRead ?? $this->createMock(UserReadRepositoryInterface::class),
            $userWrite ?? $this->createMock(UserWriteRepositoryInterface::class),
            $userTenant ?? $this->createMock(UserTenantRepositoryInterface::class),
            $scope ?? $this->createMock(TenantScopeService::class),
            $directory ?? $this->createMock(UserDirectoryGateway::class),
        );
    }
}
