<?php

namespace MintyPHP\Tests\Service\Auth;

use MintyPHP\Repository\Tenant\TenantLdapAuthRepositoryInterface;
use MintyPHP\Repository\Tenant\TenantMicrosoftAuthRepositoryInterface;
use MintyPHP\Service\Auth\AuthCryptoGateway;
use MintyPHP\Service\Auth\AuthSettingsGateway;
use MintyPHP\Service\Auth\AuthTenantGateway;
use MintyPHP\Service\Auth\TenantSsoService;
use PHPUnit\Framework\TestCase;

class TenantSsoServiceTest extends TestCase
{
    public function testGetEffectiveMicrosoftProviderConfigReturnsSsoDisabledWhenTenantAuthDisabled(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn(['enabled' => 0]);

        $service = $this->newService($repository);
        $result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);

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

    public function testGetEffectiveMicrosoftProviderConfigRejectsInvalidTenantId(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => 'invalid',
            'use_shared_app' => 1,
        ]);

        $service = $this->newService($repository);
        $result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);

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

    public function testGetEffectiveMicrosoftProviderConfigRequiresSharedCredentials(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => 1,
        ]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');

        $service = $this->newService($repository, null, $settingsGateway);
        $result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);

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

    public function testGetEffectiveMicrosoftProviderConfigRequiresOverrideClientId(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => 0,
            'client_id_override' => '',
            'client_secret_override_enc' => 'enc',
        ]);

        $service = $this->newService($repository);
        $result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);

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

    public function testGetEffectiveMicrosoftProviderConfigRejectsInvalidOverrideSecret(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => 0,
            'client_id_override' => 'client-id',
            'client_secret_override_enc' => 'enc',
        ]);

        $cryptoGateway = $this->createMock(AuthCryptoGateway::class);
        $cryptoGateway->method('decryptString')->willThrowException(new \RuntimeException('invalid secret'));

        $service = $this->newService($repository, null, null, $cryptoGateway);
        $result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);

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

    public function testGetEffectiveMicrosoftProviderConfigReturnsConfigForOverrideCredentials(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'allowed_domains' => 'example.com',
            'enforce_microsoft_login' => 1,
            'sync_profile_on_login' => 1,
            'sync_profile_fields' => 'first_name,avatar',
            'use_shared_app' => 0,
            'client_id_override' => 'client-id',
            'client_secret_override_enc' => 'enc',
        ]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('https://login.microsoftonline.com/common/v2.0');

        $cryptoGateway = $this->createMock(AuthCryptoGateway::class);
        $cryptoGateway->method('decryptString')->willReturn('secret-value');

        $service = $this->newService($repository, null, $settingsGateway, $cryptoGateway);
        $result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);

        $this->assertTrue($result['ok']);
        $this->assertSame('client-id', $result['config']['client_id']);
        $this->assertSame('secret-value', $result['config']['client_secret']);
        $this->assertTrue((bool) $result['config']['sync_profile_needs_graph']);
        $this->assertSame('https://login.microsoftonline.com/common/v2.0', $result['config']['authority']);
        $this->assertFalse((bool) $result['config']['use_shared_app']);
    }

    public function testResolveTenantLoginMethodsReturnsTenantNotFoundForInactiveTenant(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'inactive']);

        $service = $this->newService(null, $tenantGateway);
        $result = $service->resolveTenantLoginMethods(5);

        $this->assertFalse($result['local']);
        $this->assertFalse($result['microsoft']);
        $this->assertSame('tenant_not_found', $result['microsoft_reason']);
    }

    public function testResolveTenantLoginMethodsReturnsLocalOnlyWhenSsoDisabled(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn(['enabled' => 0]);

        $service = $this->newService($repository, $tenantGateway);
        $result = $service->resolveTenantLoginMethods(5);

        $this->assertTrue($result['local']);
        $this->assertFalse($result['microsoft']);
        $this->assertSame('sso_disabled', $result['microsoft_reason']);
    }

    public function testBuildMicrosoftUiStateReturnsLocalOnlyPasswordModeWhenSsoDisabled(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 0,
            'enforce_microsoft_login' => 1,
        ]);

        $service = $this->newService($repository);
        $result = $service->buildMicrosoftUiState(5);

        $this->assertSame('local_only', $result['password_mode']);
    }

    public function testBuildMicrosoftUiStateReturnsLocalAndMicrosoftPasswordModeWhenSsoEnabledWithoutEnforcement(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'enforce_microsoft_login' => 0,
        ]);

        $service = $this->newService($repository);
        $result = $service->buildMicrosoftUiState(5);

        $this->assertSame('local_and_microsoft', $result['password_mode']);
    }

    public function testBuildMicrosoftUiStateReturnsMicrosoftOnlyPasswordModeWhenEnforced(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'enforce_microsoft_login' => 1,
        ]);

        $service = $this->newService($repository);
        $result = $service->buildMicrosoftUiState(5);

        $this->assertSame('microsoft_only', $result['password_mode']);
    }

    public function testResolveTenantLoginMethodsReturnsMicrosoftOnlyWhenEnforcedAndConfigured(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'enforce_microsoft_login' => 1,
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => 1,
        ]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('shared-client');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('shared-secret');
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('https://login.microsoftonline.com/common/v2.0');

        $service = $this->newService($repository, $tenantGateway, $settingsGateway);
        $result = $service->resolveTenantLoginMethods(5);

        $this->assertFalse($result['local']);
        $this->assertTrue($result['microsoft']);
        $this->assertSame('', $result['microsoft_reason']);
    }

    public function testSaveTenantMicrosoftAuthReturnsErrorWhenTenantDoesNotExist(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(null);

        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->expects($this->never())->method('upsertByTenantId');

        $service = $this->newService($repository, $tenantGateway);
        $result = $service->saveTenantMicrosoftAuth(5, ['microsoft_enabled' => '1']);

        $this->assertFalse($result['ok']);
        $this->assertNotEmpty($result['errors']);
    }

    public function testSaveTenantMicrosoftAuthValidatesCryptoWhenOverrideSecretIsProvided(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 0,
            'use_shared_app' => 0,
            'client_secret_override_enc' => '',
        ]);
        $repository->expects($this->never())->method('upsertByTenantId');

        $cryptoGateway = $this->createMock(AuthCryptoGateway::class);
        $cryptoGateway->method('isConfigured')->willReturn(false);

        $service = $this->newService($repository, $tenantGateway, null, $cryptoGateway);
        $result = $service->saveTenantMicrosoftAuth(5, [
            'microsoft_enabled' => '1',
            'use_shared_app' => '',
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'client_id_override' => 'client-id',
            'client_secret_override' => 'new-secret',
        ]);

        $this->assertFalse($result['ok']);
        $this->assertNotEmpty($result['errors']);
    }

    public function testSaveTenantMicrosoftAuthPersistsNormalizedSharedAppPayload(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'enabled' => 0,
            'use_shared_app' => 1,
            'client_secret_override_enc' => '',
        ]);
        $repository->expects($this->once())
            ->method('upsertByTenantId')
            ->with(
                5,
                $this->callback(static function (array $data): bool {
                    return $data['enabled'] === true
                        && $data['enforce_microsoft_login'] === true
                        && $data['sync_profile_on_login'] === true
                        && $data['sync_profile_fields'] === 'first_name,last_name'
                        && $data['entra_tenant_id'] === '11111111-1111-1111-1111-111111111111'
                        && $data['allowed_domains'] === 'example.com,test.org'
                        && $data['use_shared_app'] === true
                        && $data['client_id_override'] === null
                        && $data['client_secret_override_enc'] === null;
                })
            )
            ->willReturn(true);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('shared-client');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('shared-secret');

        $service = $this->newService($repository, $tenantGateway, $settingsGateway);
        $result = $service->saveTenantMicrosoftAuth(5, [
            'microsoft_enabled' => '1',
            'enforce_microsoft_login' => '1',
            'sync_profile_on_login' => '1',
            'sync_profile_fields' => [],
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'allowed_domains' => "example.com, invalid, test.org\nexample.com",
            'use_shared_app' => '1',
            'client_id_override' => 'ignored',
            'client_secret_override' => '',
        ]);

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

    // ---------------------------------------------------------------
    // shouldAutoRememberOnMicrosoftLogin — policy resolution
    // ---------------------------------------------------------------

    public function testShouldAutoRememberReturnsTrueWhenForceOn(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'auto_remember_mode' => 1,
        ]);

        $service = $this->newService($repository);
        $this->assertTrue($service->shouldAutoRememberOnMicrosoftLogin(5));
    }

    public function testShouldAutoRememberReturnsFalseWhenForceOff(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'auto_remember_mode' => 0,
        ]);

        $service = $this->newService($repository);
        $this->assertFalse($service->shouldAutoRememberOnMicrosoftLogin(5));
    }

    public function testShouldAutoRememberInheritUsesGlobalDefaultTrue(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'auto_remember_mode' => null,
        ]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('');
        $settingsGateway->method('isMicrosoftAutoRememberDefault')->willReturn(true);

        $service = $this->newService($repository, null, $settingsGateway);
        $this->assertTrue($service->shouldAutoRememberOnMicrosoftLogin(5));
    }

    public function testShouldAutoRememberInheritUsesGlobalDefaultFalse(): void
    {
        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([
            'auto_remember_mode' => null,
        ]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('');
        $settingsGateway->method('isMicrosoftAutoRememberDefault')->willReturn(false);

        $service = $this->newService($repository, null, $settingsGateway);
        $this->assertFalse($service->shouldAutoRememberOnMicrosoftLogin(5));
    }

    public function testSaveTenantMicrosoftAuthPersistsAutoRememberMode(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([]);
        $repository->expects($this->once())
            ->method('upsertByTenantId')
            ->with(
                5,
                $this->callback(static function (array $data): bool {
                    return $data['auto_remember_mode'] === 1;
                })
            )
            ->willReturn(true);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('client-secret');
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('');

        $service = $this->newService($repository, $tenantGateway, $settingsGateway);
        $result = $service->saveTenantMicrosoftAuth(5, [
            'microsoft_enabled' => '1',
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => '1',
            'sync_profile_on_login' => '',
            'sync_profile_fields' => [],
            'allowed_domains' => '',
            'enforce_microsoft_login' => '',
            'client_id_override' => '',
            'client_secret_override' => '',
            'microsoft_auto_remember_mode' => '1',
        ]);

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

    public function testSaveTenantMicrosoftAuthPersistsInheritAsNull(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $repository->method('findByTenantId')->willReturn([]);
        $repository->expects($this->once())
            ->method('upsertByTenantId')
            ->with(
                5,
                $this->callback(static function (array $data): bool {
                    return $data['auto_remember_mode'] === null;
                })
            )
            ->willReturn(true);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('client-secret');
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('');

        $service = $this->newService($repository, $tenantGateway, $settingsGateway);
        $result = $service->saveTenantMicrosoftAuth(5, [
            'microsoft_enabled' => '1',
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => '1',
            'sync_profile_on_login' => '',
            'sync_profile_fields' => [],
            'allowed_domains' => '',
            'enforce_microsoft_login' => '',
            'client_id_override' => '',
            'client_secret_override' => '',
            'microsoft_auto_remember_mode' => 'inherit',
        ]);

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

    // ── discoverTenantsByEmailDomain ────────────────────────────────

    public function testDiscoverTenantsByEmailDomainReturnsMatchingMicrosoftTenant(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active', 'uuid' => 'abc', 'description' => 'Acme']);

        $microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $microsoftRepo->expects($this->any())->method('findEnabledWithExplicitDomain')->with('acme.com')->willReturn([
            ['tenant_id' => 5, 'allowed_domains' => 'acme.com'],
        ]);
        $microsoftRepo->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => 1,
        ]);

        $ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepo->method('findEnabledWithExplicitDomain')->willReturn([]);
        $ldapRepo->method('findByTenantId')->willReturn(['enabled' => 0]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('secret');
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('https://login.microsoftonline.com/common/v2.0');

        $service = $this->newService($microsoftRepo, $tenantGateway, $settingsGateway, null, $ldapRepo);
        $result = $service->discoverTenantsByEmailDomain('user@acme.com');

        $this->assertCount(1, $result);
        $this->assertSame(5, $result[0]['id']);
        $this->assertFalse($result[0]['methods']['local']);
        $this->assertTrue($result[0]['methods']['microsoft']);
    }

    public function testDiscoverTenantsByEmailDomainReturnsEmptyForUnknownDomain(): void
    {
        $microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $microsoftRepo->method('findEnabledWithExplicitDomain')->willReturn([]);

        $ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepo->method('findEnabledWithExplicitDomain')->willReturn([]);

        $service = $this->newService($microsoftRepo, null, null, null, $ldapRepo);
        $result = $service->discoverTenantsByEmailDomain('user@unknown.org');

        $this->assertSame([], $result);
    }

    public function testDiscoverTenantsByEmailDomainExcludesInactiveTenants(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'inactive']);

        $microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $microsoftRepo->method('findEnabledWithExplicitDomain')->willReturn([
            ['tenant_id' => 5, 'allowed_domains' => 'acme.com'],
        ]);

        $ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepo->method('findEnabledWithExplicitDomain')->willReturn([]);

        $service = $this->newService($microsoftRepo, $tenantGateway, null, null, $ldapRepo);
        $result = $service->discoverTenantsByEmailDomain('user@acme.com');

        $this->assertSame([], $result);
    }

    public function testDiscoverTenantsByEmailDomainForcesLocalFalse(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active', 'uuid' => 'abc', 'description' => 'Acme']);

        $microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $microsoftRepo->method('findEnabledWithExplicitDomain')->willReturn([
            ['tenant_id' => 5, 'allowed_domains' => 'acme.com'],
        ]);
        // Microsoft disabled, but LDAP should still work — and local must be false
        $microsoftRepo->method('findByTenantId')->willReturn(['enabled' => 0]);

        $ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepo->method('findEnabledWithExplicitDomain')->willReturn([
            ['tenant_id' => 5, 'allowed_domains' => 'acme.com'],
        ]);
        $ldapRepo->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'host' => 'ldap.acme.com',
            'base_dn' => 'DC=acme,DC=com',
            'bind_dn_enc' => '',
            'bind_password_enc' => '',
        ]);

        $service = $this->newService($microsoftRepo, $tenantGateway, null, null, $ldapRepo);
        $result = $service->discoverTenantsByEmailDomain('user@acme.com');

        $this->assertCount(1, $result);
        $this->assertFalse($result[0]['methods']['local']);
        $this->assertTrue($result[0]['methods']['ldap']);
    }

    public function testDiscoverTenantsByEmailDomainExcludesTenantWithNoFunctionalSso(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $microsoftRepo->method('findEnabledWithExplicitDomain')->willReturn([
            ['tenant_id' => 5, 'allowed_domains' => 'acme.com'],
        ]);
        // Microsoft enabled but misconfigured (invalid tenant ID)
        $microsoftRepo->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => 'invalid',
            'use_shared_app' => 1,
        ]);

        $ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepo->method('findEnabledWithExplicitDomain')->willReturn([]);
        $ldapRepo->method('findByTenantId')->willReturn(['enabled' => 0]);

        $service = $this->newService($microsoftRepo, $tenantGateway, null, null, $ldapRepo);
        $result = $service->discoverTenantsByEmailDomain('user@acme.com');

        $this->assertSame([], $result);
    }

    public function testDiscoverTenantsByEmailDomainReturnsEmptyForInvalidEmail(): void
    {
        $service = $this->newService();
        $this->assertSame([], $service->discoverTenantsByEmailDomain('not-an-email'));
        $this->assertSame([], $service->discoverTenantsByEmailDomain(''));
    }

    public function testDiscoverTenantsByEmailDomainReturnsMultipleTenants(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturnCallback(static function (int $id): ?array {
            return match ($id) {
                5 => ['id' => 5, 'status' => 'active', 'uuid' => 'u5', 'description' => 'Tenant A'],
                7 => ['id' => 7, 'status' => 'active', 'uuid' => 'u7', 'description' => 'Tenant B'],
                default => null,
            };
        });

        $microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $microsoftRepo->method('findEnabledWithExplicitDomain')->willReturn([
            ['tenant_id' => 5, 'allowed_domains' => 'shared.com'],
            ['tenant_id' => 7, 'allowed_domains' => 'shared.com'],
        ]);
        $microsoftRepo->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
            'use_shared_app' => 1,
        ]);

        $ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepo->method('findEnabledWithExplicitDomain')->willReturn([]);
        $ldapRepo->method('findByTenantId')->willReturn(['enabled' => 0]);

        $settingsGateway = $this->createMock(AuthSettingsGateway::class);
        $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
        $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('secret');
        $settingsGateway->method('getMicrosoftAuthority')->willReturn('https://login.microsoftonline.com/common/v2.0');

        $service = $this->newService($microsoftRepo, $tenantGateway, $settingsGateway, null, $ldapRepo);
        $result = $service->discoverTenantsByEmailDomain('user@shared.com');

        $this->assertCount(2, $result);
        $ids = array_column($result, 'id');
        $this->assertContains(5, $ids);
        $this->assertContains(7, $ids);
    }

    // ── LDAP tests ────────────────────────────────────────────────────

    public function testGetTenantLdapAuthReturnsDefaults(): void
    {
        $ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepository->method('findByTenantId')->willReturn(null);

        $service = $this->newService(null, null, null, null, $ldapRepository);
        $result = $service->getTenantLdapAuth(1);

        $this->assertFalse($result['enabled']);
        $this->assertFalse($result['enforce_ldap_login']);
        $this->assertSame('', $result['host']);
        $this->assertSame(389, $result['port']);
        $this->assertSame('starttls', $result['encryption_mode']);
    }

    public function testGetEffectiveLdapProviderConfigReturnsDisabledWhenNotEnabled(): void
    {
        $ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepository->method('findByTenantId')->willReturn(['enabled' => 0]);

        $service = $this->newService(null, null, null, null, $ldapRepository);
        $result = $service->getEffectiveLdapProviderConfig(['id' => 5]);

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

    public function testGetEffectiveLdapProviderConfigReturnsHostMissing(): void
    {
        $ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'host' => '',
            'base_dn' => 'DC=example,DC=com',
        ]);

        $service = $this->newService(null, null, null, null, $ldapRepository);
        $result = $service->getEffectiveLdapProviderConfig(['id' => 5]);

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

    public function testGetEffectiveLdapProviderConfigReturnsBaseDnMissing(): void
    {
        $ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'host' => 'ldap.example.com',
            'base_dn' => '',
        ]);

        $service = $this->newService(null, null, null, null, $ldapRepository);
        $result = $service->getEffectiveLdapProviderConfig(['id' => 5]);

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

    public function testGetEffectiveLdapProviderConfigSuccessNoBindCredentials(): void
    {
        $ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepository->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'host' => 'ldap.example.com',
            'port' => 389,
            'encryption_mode' => 'starttls',
            'base_dn' => 'DC=example,DC=com',
            'bind_dn_enc' => '',
            'bind_password_enc' => '',
            'user_search_filter' => '(&(objectClass=user)(sAMAccountName=%s))',
        ]);

        $service = $this->newService(null, null, null, null, $ldapRepository);
        $result = $service->getEffectiveLdapProviderConfig(['id' => 5]);

        $this->assertTrue($result['ok']);
        $this->assertSame('ldap', $result['config']['provider']);
        $this->assertSame('ldap.example.com', $result['config']['host']);
    }

    public function testResolveTenantLoginMethodsIncludesLdap(): void
    {
        $tenantGateway = $this->createMock(AuthTenantGateway::class);
        $tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);

        $microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $microsoftRepo->method('findByTenantId')->willReturn(['enabled' => 0]);

        $ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $ldapRepo->method('findByTenantId')->willReturn([
            'enabled' => 1,
            'host' => 'ldap.example.com',
            'base_dn' => 'DC=example,DC=com',
            'bind_dn_enc' => '',
            'bind_password_enc' => '',
        ]);

        $service = $this->newService($microsoftRepo, $tenantGateway, null, null, $ldapRepo);
        $result = $service->resolveTenantLoginMethods(5);

        $this->assertTrue($result['local']);
        $this->assertFalse($result['microsoft']);
        $this->assertTrue($result['ldap']);
    }

    public function testNormalizeLdapAttributeMapParsesJson(): void
    {
        $service = $this->newService();
        $map = $service->normalizeLdapAttributeMap('{"first_name":"givenName","email":"mail"}');
        $this->assertSame('givenName', $map['first_name']);
        $this->assertSame('mail', $map['email']);
    }

    public function testNormalizeLdapAttributeMapReturnsDefaultForInvalidJson(): void
    {
        $service = $this->newService();
        $map = $service->normalizeLdapAttributeMap('not json');
        $this->assertArrayHasKey('first_name', $map);
        $this->assertSame('givenName', $map['first_name']);
    }

    private function newService(
        ?TenantMicrosoftAuthRepositoryInterface $repository = null,
        ?AuthTenantGateway $tenantGateway = null,
        ?AuthSettingsGateway $settingsGateway = null,
        ?AuthCryptoGateway $cryptoGateway = null,
        ?TenantLdapAuthRepositoryInterface $ldapRepository = null
    ): TenantSsoService {
        $repository = $repository ?? $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
        $ldapRepository = $ldapRepository ?? $this->createMock(TenantLdapAuthRepositoryInterface::class);
        $tenantGateway = $tenantGateway ?? $this->createMock(AuthTenantGateway::class);
        if ($settingsGateway === null) {
            $settingsGateway = $this->createMock(AuthSettingsGateway::class);
            $settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
            $settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
            $settingsGateway->method('getMicrosoftAuthority')->willReturn('');
        }
        if ($cryptoGateway === null) {
            $cryptoGateway = $this->createMock(AuthCryptoGateway::class);
            $cryptoGateway->method('isConfigured')->willReturn(true);
            $cryptoGateway->method('decryptString')->willReturn('decrypted-secret');
            $cryptoGateway->method('encryptString')->willReturn('encrypted-secret');
        }

        return new TenantSsoService(
            $tenantGateway,
            $repository,
            $ldapRepository,
            $settingsGateway,
            $cryptoGateway
        );
    }
}
