<?php

namespace MintyPHP\Tests\Service\CustomField;

use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepositoryInterface;
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface;
use MintyPHP\Repository\CustomField\UserCustomFieldValueOptionRepositoryInterface;
use MintyPHP\Repository\CustomField\UserCustomFieldValueRepositoryInterface;
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
use MintyPHP\Service\Tenant\TenantScopeService;
use PHPUnit\Framework\TestCase;

final class UserCustomFieldValueServiceTest extends TestCase
{
    private const UUID_A = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
    private const UUID_B = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
    private const UUID_C = 'cccccccc-cccc-cccc-cccc-cccccccccccc';

    // --- validateForTenants ------------------------------------------------

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

        $this->assertSame(['ok' => true, 'errors' => []], $service->validateForTenants([1], [], false));
    }

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

        $this->assertSame(['ok' => true, 'errors' => []], $service->validateForTenants([0, -1], [], true));
    }

    public function testValidateForTenantsReturnsPreparedErrorsWhenInvalidValue(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 5, 'tenant_id' => 1, 'label' => 'Score', 'type' => 'date'],
        ]);
        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([]);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
        );

        $result = $service->validateForTenants([1], [
            'custom_field_values' => ['5' => 'not-a-date'],
        ], true);

        $this->assertFalse($result['ok']);
        $this->assertContains('Invalid value for Score', $result['errors']);
    }

    public function testValidateForTenantsReturnsOkWhenClean(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 5, 'tenant_id' => 1, 'label' => 'Nickname', 'type' => 'text'],
        ]);
        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([]);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
        );

        $result = $service->validateForTenants([1], [
            'custom_field_values' => ['5' => 'Fred'],
        ], true);

        $this->assertSame(['ok' => true, 'errors' => []], $result);
    }

    // --- buildDefinitionsByTenant ------------------------------------------

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

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

    public function testBuildDefinitionsByTenantReturnsEmptyWhenNoDefinitions(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->expects($this->once())
            ->method('listByTenantIds')
            ->with([1, 2], true)
            ->willReturn([]);

        $service = $this->makeService(definitionRepository: $definitionRepo);

        $this->assertSame([], $service->buildDefinitionsByTenant([1, 2]));
    }

    public function testBuildDefinitionsByTenantGroupsByTenantAndMergesOptions(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 10, 'tenant_id' => 1, 'type' => 'select'],
            ['id' => 11, 'tenant_id' => 2, 'type' => 'text'],
        ]);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([
            ['definition_id' => 10, 'id' => 100, 'option_key' => 'a', 'label' => 'A'],
        ]);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
        );

        $result = $service->buildDefinitionsByTenant([1, 2]);

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

    // --- buildUserValueMap -------------------------------------------------

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

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

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

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

    public function testBuildUserValueMapReturnsEmptyWhenRepoHasNoRows(): void
    {
        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('listByUserAndDefinitionIds')->willReturn([]);

        $service = $this->makeService(valueRepository: $valueRepo);

        $this->assertSame([], $service->buildUserValueMap(7, [1, 2]));
    }

    public function testBuildUserValueMapMergesOptionIdsForMultiselectValues(): void
    {
        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('listByUserAndDefinitionIds')->willReturn([
            ['id' => 100, 'definition_id' => 10, 'value_text' => 'Fred', 'value_bool' => null, 'value_date' => null, 'option_id' => null],
            ['id' => 101, 'definition_id' => 11, 'value_text' => null, 'value_bool' => 1, 'value_date' => null, 'option_id' => null],
        ]);

        $valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
        $valueOptionRepo->method('listOptionIdsByValueIds')->willReturn([
            100 => [200, 201],
        ]);

        $service = $this->makeService(
            valueRepository: $valueRepo,
            valueOptionRepository: $valueOptionRepo,
        );

        $result = $service->buildUserValueMap(7, [10, 11]);

        $this->assertSame([200, 201], $result[10]['option_ids']);
        $this->assertSame('Fred', $result[10]['value_text']);
        $this->assertSame([], $result[11]['option_ids']);
        $this->assertSame(1, $result[11]['value_bool']);
    }

    // --- buildPublicValuesByTenant -----------------------------------------

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

        $this->assertSame([], $service->buildPublicValuesByTenant(0, [['id' => 1]]));
    }

    public function testBuildPublicValuesByTenantFiltersByTenantScopeId(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        // Only tenant 2 is resolved because tenantScopeId=2 filters out tenant 1.
        $definitionRepo->expects($this->once())
            ->method('listByTenantIds')
            ->with([2], true)
            ->willReturn([]);

        $service = $this->makeService(definitionRepository: $definitionRepo);

        $result = $service->buildPublicValuesByTenant(7, [
            ['id' => 1, 'uuid' => 't1', 'description' => 'T1', 'status' => 'active'],
            ['id' => 2, 'uuid' => 't2', 'description' => 'T2', 'status' => 'active'],
        ], 2);

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

    public function testBuildPublicValuesByTenantAssemblesFieldsWithValues(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 10, 'tenant_id' => 1, 'uuid' => self::UUID_A, 'field_key' => 'nick', 'label' => 'Nickname', 'type' => 'text', 'is_filterable' => 0],
            ['id' => 11, 'tenant_id' => 1, 'uuid' => self::UUID_B, 'field_key' => 'prio', 'label' => 'Priority', 'type' => 'select', 'is_filterable' => 1],
        ]);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([
            ['definition_id' => 11, 'id' => 200, 'option_key' => 'low', 'label' => 'Low'],
            ['definition_id' => 11, 'id' => 201, 'option_key' => 'high', 'label' => 'High'],
        ]);

        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('listByUserAndDefinitionIds')->willReturn([
            ['id' => 500, 'definition_id' => 10, 'value_text' => 'Fred', 'value_bool' => null, 'value_date' => null, 'option_id' => null],
            ['id' => 501, 'definition_id' => 11, 'value_text' => null, 'value_bool' => null, 'value_date' => null, 'option_id' => 201],
        ]);

        $valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
        $valueOptionRepo->method('listOptionIdsByValueIds')->willReturn([]);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
            valueRepository: $valueRepo,
            valueOptionRepository: $valueOptionRepo,
        );

        $result = $service->buildPublicValuesByTenant(7, [
            ['id' => 1, 'uuid' => 't1', 'description' => 'Tenant 1', 'status' => 'active'],
        ]);

        $this->assertCount(1, $result);
        $this->assertSame('Tenant 1', $result[0]['tenant']['description']);
        $this->assertCount(2, $result[0]['fields']);
        $this->assertSame('Fred', $result[0]['fields'][0]['value']);
        $this->assertSame('high', $result[0]['fields'][1]['value']);
        $this->assertCount(2, $result[0]['fields'][1]['options']);
    }

    // --- syncForUser -------------------------------------------------------

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

        $result = $service->syncForUser(0, [1], [], true);

        $this->assertFalse($result['ok']);
        $this->assertSame(['User not found'], $result['errors']);
    }

    public function testSyncForUserFailsWhenOutsideCleanupFails(): void
    {
        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(false);

        $service = $this->makeService(valueRepository: $valueRepo);

        $result = $service->syncForUser(7, [1], [], true);

        $this->assertFalse($result['ok']);
        $this->assertContains('Custom field values can not be saved', $result['errors']);
    }

    public function testSyncForUserSkipsEditsWhenNotEditable(): void
    {
        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
        $valueRepo->expects($this->never())->method('upsertScalarValue');

        $service = $this->makeService(valueRepository: $valueRepo);

        $result = $service->syncForUser(7, [1], ['custom_field_values' => ['5' => 'Fred']], false);

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

    public function testSyncForUserWritesTextValue(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 5, 'tenant_id' => 1, 'label' => 'Nickname', 'type' => 'text'],
        ]);
        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([]);

        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
        $valueRepo->expects($this->once())
            ->method('upsertScalarValue')
            ->with(7, 5, $this->callback(static fn (array $data): bool => ($data['value_text'] ?? null) === 'Fred'))
            ->willReturn(500);

        $valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
        $valueOptionRepo->expects($this->once())
            ->method('replaceForValueId')
            ->with(500, [])
            ->willReturn(true);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
            valueRepository: $valueRepo,
            valueOptionRepository: $valueOptionRepo,
        );

        $result = $service->syncForUser(7, [1], [
            'custom_field_values' => ['5' => 'Fred'],
        ], true);

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

    public function testSyncForUserDeletesEmptyScalarValues(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 5, 'tenant_id' => 1, 'label' => 'Nickname', 'type' => 'text'],
        ]);
        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([]);

        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
        $valueRepo->expects($this->once())
            ->method('deleteByUserAndDefinitionIds')
            ->with(7, [5])
            ->willReturn(true);
        $valueRepo->expects($this->never())->method('upsertScalarValue');

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
            valueRepository: $valueRepo,
        );

        $result = $service->syncForUser(7, [1], [
            'custom_field_values' => ['5' => '  '],
        ], true);

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

    public function testSyncForUserRejectsInvalidDateValue(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 5, 'tenant_id' => 1, 'label' => 'Due', 'type' => 'date'],
        ]);
        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([]);

        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
            valueRepository: $valueRepo,
        );

        $result = $service->syncForUser(7, [1], [
            'custom_field_values' => ['5' => 'not-a-date'],
        ], true);

        $this->assertFalse($result['ok']);
        $this->assertContains('Invalid value for Due', $result['errors']);
    }

    public function testSyncForUserWritesMultiselectOptionIds(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 5, 'tenant_id' => 1, 'label' => 'Tags', 'type' => 'multiselect'],
        ]);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([
            ['definition_id' => 5, 'id' => 200, 'active' => 1, 'option_key' => 'x', 'label' => 'X'],
            ['definition_id' => 5, 'id' => 201, 'active' => 1, 'option_key' => 'y', 'label' => 'Y'],
        ]);

        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
        $valueRepo->expects($this->once())
            ->method('upsertScalarValue')
            ->with(7, 5, $this->callback(static fn (array $d): bool => ($d['option_id'] ?? null) === null && ($d['value_text'] ?? null) === null))
            ->willReturn(500);

        $valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
        $valueOptionRepo->expects($this->once())
            ->method('replaceForValueId')
            ->with(500, [200, 201])
            ->willReturn(true);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
            valueRepository: $valueRepo,
            valueOptionRepository: $valueOptionRepo,
        );

        $result = $service->syncForUser(7, [1], [
            'custom_field_values_multi' => ['5' => [200, 201]],
        ], true);

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

    public function testSyncForUserRejectsMultiselectValueNotInActiveOptions(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantIds')->willReturn([
            ['id' => 5, 'tenant_id' => 1, 'label' => 'Tags', 'type' => 'multiselect'],
        ]);
        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([
            ['definition_id' => 5, 'id' => 200, 'active' => 1],
        ]);

        $valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
        $valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);

        $service = $this->makeService(
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
            valueRepository: $valueRepo,
        );

        $result = $service->syncForUser(7, [1], [
            'custom_field_values_multi' => ['5' => [200, 999]],
        ], true);

        $this->assertFalse($result['ok']);
        $this->assertContains('Invalid value for Tags', $result['errors']);
    }

    // --- extractCustomFieldFilterSpec -------------------------------------

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

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listFilterableByTenantIds')->willReturn([]);

        $service = $this->makeService(
            userScopeGateway: $scope,
            definitionRepository: $definitionRepo,
        );

        $this->assertSame(
            ['definitions' => [], 'filters' => [], 'query' => []],
            $service->extractCustomFieldFilterSpec(['cf_' . self::UUID_A => '123'], 7)
        );
    }

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

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listFilterableByTenantIds')->willReturn([
            ['id' => 10, 'uuid' => self::UUID_A, 'type' => 'select'],
            ['id' => 11, 'uuid' => self::UUID_B, 'type' => 'boolean'],
        ]);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([
            ['definition_id' => 10, 'id' => 200],
        ]);

        $service = $this->makeService(
            userScopeGateway: $scope,
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
        );

        $result = $service->extractCustomFieldFilterSpec([
            'cf_' . self::UUID_A => '200',
            'cf_' . self::UUID_B => 'yes',
            'cf_' . self::UUID_C => 'ignored', // UUID_C not among definitions
        ], 7);

        $this->assertSame(['10' => 200], $this->keyedAsStringInt($result['filters']['select']));
        $this->assertSame(['11' => 1], $this->keyedAsStringInt($result['filters']['boolean']));
        $this->assertArrayNotHasKey('cf_' . self::UUID_C, $result['query']);
    }

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

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listFilterableByTenantIds')->willReturn([
            ['id' => 10, 'uuid' => self::UUID_A, 'type' => 'multiselect'],
            ['id' => 11, 'uuid' => self::UUID_B, 'type' => 'date'],
        ]);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->method('listByDefinitionIds')->willReturn([
            ['definition_id' => 10, 'id' => 200],
            ['definition_id' => 10, 'id' => 201],
        ]);

        $service = $this->makeService(
            userScopeGateway: $scope,
            definitionRepository: $definitionRepo,
            optionRepository: $optionRepo,
        );

        $result = $service->extractCustomFieldFilterSpec([
            'cfm_' . self::UUID_A => '200,201,999', // 999 not allowed — should be filtered
            'cfd_' . self::UUID_B . '_from' => '2026-01-01',
            'cfd_' . self::UUID_B . '_to' => 'not-a-date', // invalid — ignored
        ], 7);

        $this->assertSame([200, 201], $result['filters']['multiselect'][10]);
        $this->assertSame('2026-01-01', $result['filters']['date'][11]['from']);
        $this->assertNull($result['filters']['date'][11]['to']);
    }

    // --- helpers -----------------------------------------------------------

    /** @param array<int, int> $map @return array<string, int> */
    private function keyedAsStringInt(array $map): array
    {
        $out = [];
        foreach ($map as $k => $v) {
            $out[(string) $k] = (int) $v;
        }
        return $out;
    }

    private function makeService(
        ?TenantScopeService $userScopeGateway = null,
        ?TenantCustomFieldDefinitionRepositoryInterface $definitionRepository = null,
        ?TenantCustomFieldOptionRepositoryInterface $optionRepository = null,
        ?UserCustomFieldValueRepositoryInterface $valueRepository = null,
        ?UserCustomFieldValueOptionRepositoryInterface $valueOptionRepository = null,
    ): UserCustomFieldValueService {
        return new UserCustomFieldValueService(
            $userScopeGateway ?? $this->createMock(TenantScopeService::class),
            $definitionRepository ?? $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class),
            $optionRepository ?? $this->createMock(TenantCustomFieldOptionRepositoryInterface::class),
            $valueRepository ?? $this->createMock(UserCustomFieldValueRepositoryInterface::class),
            $valueOptionRepository ?? $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class),
        );
    }
}
