<?php

namespace MintyPHP\Tests\Service\CustomField;

use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepositoryInterface;
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\CustomField\TenantCustomFieldService;
use PHPUnit\Framework\TestCase;

final class TenantCustomFieldServiceTest extends TestCase
{
    public function testListForTenantReturnsEmptyForInvalidTenantId(): void
    {
        $service = $this->makeService();

        $this->assertSame([], $service->listForTenant(0));
        $this->assertSame([], $service->listForTenant(-5));
    }

    public function testListForTenantReturnsEmptyWhenNoDefinitions(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->expects($this->once())
            ->method('listByTenantId')
            ->with(10, false)
            ->willReturn([]);

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

        $this->assertSame([], $service->listForTenant(10));
    }

    public function testListForTenantMergesOptionsIntoDefinitions(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('listByTenantId')->willReturn([
            ['id' => 1, 'label' => 'Alpha', 'type' => 'select'],
            ['id' => 2, 'label' => 'Beta', 'type' => 'text'],
        ]);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->expects($this->once())
            ->method('listByDefinitionIds')
            ->with([1, 2], false)
            ->willReturn([
                ['definition_id' => 1, 'option_key' => 'a', 'label' => 'A'],
                ['definition_id' => 1, 'option_key' => 'b', 'label' => 'B'],
            ]);

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

        $result = $service->listForTenant(10);

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

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

        $result = $service->createForTenant(0, ['label' => 'Foo'], 1);

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

    public function testCreateForTenantRejectsUnknownTenant(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->expects($this->once())->method('find')->with(99)->willReturn(null);

        $service = $this->makeService(tenantRepository: $tenantRepo);

        $result = $service->createForTenant(99, ['label' => 'Foo'], 1);

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

    public function testCreateForTenantReturnsValidationErrorsForEmptyLabel(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('find')->willReturn(['id' => 10]);

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn(null);
        $definitionRepo->expects($this->never())->method('create');

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

        $result = $service->createForTenant(10, ['label' => '', 'type' => 'text'], 1);

        $this->assertFalse($result['ok']);
        $this->assertContains('Label cannot be empty', $result['errors']);
    }

    public function testCreateForTenantRejectsInvalidFieldType(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('find')->willReturn(['id' => 10]);

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn(null);

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

        $result = $service->createForTenant(10, ['label' => 'Foo', 'type' => 'rocket'], 1);

        $this->assertFalse($result['ok']);
        $this->assertContains('Field type is invalid', $result['errors']);
    }

    public function testCreateForTenantReturnsEmptyKeyErrorWhenAllCandidatesCollide(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('find')->willReturn(['id' => 10]);

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        // Every candidate hits a different existing record, exhausting the 10000-candidate guard;
        // resolveFieldKey then returns '' and validateForm flags the empty key.
        $definitionRepo->method('findByTenantIdAndKey')->willReturnCallback(
            static fn (int $tenantId, string $key): array => ['id' => 999, 'tenant_id' => $tenantId, 'field_key' => $key]
        );

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

        $result = $service->createForTenant(10, ['label' => 'Foo', 'type' => 'text'], 1);

        $this->assertFalse($result['ok']);
        $this->assertContains('Field key cannot be empty', $result['errors']);
    }

    public function testUpdateByUuidRejectsDuplicateFieldKeyFromDifferentRecord(): void
    {
        $existing = ['id' => 5, 'tenant_id' => 10, 'field_key' => 'priority'];

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn($existing);
        // resolveFieldKey sees id=5 for 'priority' (== excludeId) so returns 'priority' unchanged.
        // validateForm re-queries and gets the SAME key owned by a DIFFERENT record id (7) — duplicate.
        $definitionRepo->method('findByTenantIdAndKey')
            ->willReturnOnConsecutiveCalls(
                ['id' => 5, 'tenant_id' => 10, 'field_key' => 'priority'],
                ['id' => 7, 'tenant_id' => 10, 'field_key' => 'priority'],
            );
        $definitionRepo->expects($this->never())->method('update');

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

        $result = $service->updateByUuid('uuid-of-5', [
            'label' => 'Priority',
            'type' => 'text',
            'field_key' => 'priority',
        ], 7);

        $this->assertFalse($result['ok']);
        $this->assertContains('Field key already exists', $result['errors']);
    }

    public function testCreateForTenantRejectsSelectWithoutOptions(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('find')->willReturn(['id' => 10]);

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn(null);

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

        $result = $service->createForTenant(10, [
            'label' => 'Prio',
            'type' => 'select',
            'options_text' => '',
        ], 1);

        $this->assertFalse($result['ok']);
        $this->assertContains('Options are required for this field type', $result['errors']);
    }

    public function testCreateForTenantInsertsTextFieldWithoutReplacingOptions(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('find')->willReturn(['id' => 10]);

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn(null);
        $definitionRepo->method('listByTenantId')->willReturn([]);
        $definitionRepo->expects($this->once())
            ->method('create')
            ->with($this->callback(static function (array $data): bool {
                return ($data['tenant_id'] ?? null) === 10
                    && ($data['label'] ?? null) === 'Nickname'
                    && ($data['type'] ?? null) === 'text'
                    && ($data['sort_order'] ?? null) === 100
                    && ($data['created_by'] ?? null) === 7;
            }))
            ->willReturn(42);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->expects($this->never())->method('replaceForDefinition');

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

        $result = $service->createForTenant(10, [
            'label' => 'Nickname',
            'type' => 'text',
        ], 7);

        $this->assertTrue($result['ok']);
        $this->assertSame(42, $result['id']);
    }

    public function testCreateForTenantInsertsSelectFieldAndReplacesOptions(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('find')->willReturn(['id' => 10]);

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn(null);
        $definitionRepo->method('listByTenantId')->willReturn([
            ['id' => 1, 'sort_order' => 200],
        ]);
        $definitionRepo->method('create')->willReturn(99);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->expects($this->once())
            ->method('replaceForDefinition')
            ->with(99, $this->callback(static function (array $options): bool {
                return count($options) === 2
                    && $options[0]['option_key'] === 'low'
                    && $options[1]['option_key'] === 'high';
            }))
            ->willReturn(true);

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

        $result = $service->createForTenant(10, [
            'label' => 'Priority',
            'type' => 'select',
            'is_filterable' => 1,
            'options_text' => "low|Low\nhigh|High",
        ], 7);

        $this->assertTrue($result['ok']);
        $this->assertSame(99, $result['id']);
        // No explicit sort_order input → form keeps null; createForTenant fills the insert via nextSortOrder (200+10).
        $this->assertNull($result['form']['sort_order']);
    }

    public function testCreateForTenantReturnsInsertFailureWhenRepoReturnsFalse(): void
    {
        $tenantRepo = $this->createMock(TenantRepository::class);
        $tenantRepo->method('find')->willReturn(['id' => 10]);

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn(null);
        $definitionRepo->method('listByTenantId')->willReturn([]);
        $definitionRepo->method('create')->willReturn(false);

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

        $result = $service->createForTenant(10, ['label' => 'Foo', 'type' => 'text'], 1);

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

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

        $result = $service->updateByUuid('   ', [], 1);

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

    public function testUpdateByUuidRejectsUnknownUuid(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn(null);

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

        $result = $service->updateByUuid('missing-uuid', [], 1);

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

    public function testUpdateByUuidPreservesExistingFieldKeyWhenSameRecord(): void
    {
        $existing = ['id' => 5, 'tenant_id' => 10, 'field_key' => 'priority', 'sort_order' => 30];

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn($existing);
        $definitionRepo->method('findByTenantIdAndKey')->willReturnCallback(
            static fn (int $tenantId, string $key): array => ['id' => 5, 'tenant_id' => $tenantId, 'field_key' => $key]
        );
        $definitionRepo->expects($this->once())
            ->method('update')
            ->with(5, $this->callback(static function (array $data): bool {
                return ($data['field_key'] ?? null) === 'priority'
                    && ($data['type'] ?? null) === 'text'
                    && ($data['modified_by'] ?? null) === 7;
            }))
            ->willReturn(true);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->expects($this->once())
            ->method('deleteByDefinitionId')
            ->with(5)
            ->willReturn(true);

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

        $result = $service->updateByUuid('uuid-of-5', [
            'label' => 'Priority',
            'type' => 'text',
            'field_key' => 'priority',
        ], 7);

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

    public function testUpdateByUuidReplacesOptionsWhenSelectType(): void
    {
        $existing = ['id' => 5, 'tenant_id' => 10, 'field_key' => 'priority', 'sort_order' => 30];

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn($existing);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn($existing);
        $definitionRepo->method('update')->willReturn(true);

        $optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
        $optionRepo->expects($this->once())
            ->method('replaceForDefinition')
            ->with(5, $this->callback(static fn ($options): bool => is_array($options)))
            ->willReturn(true);
        $optionRepo->expects($this->never())->method('deleteByDefinitionId');

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

        $result = $service->updateByUuid('uuid-of-5', [
            'label' => 'Priority',
            'type' => 'select',
            'options_text' => 'low|Low',
            'field_key' => 'priority',
        ], 7);

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

    public function testUpdateByUuidReturnsUpdateFailureWhenRepoReturnsFalse(): void
    {
        $existing = ['id' => 5, 'tenant_id' => 10, 'field_key' => 'priority'];

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn($existing);
        $definitionRepo->method('findByTenantIdAndKey')->willReturn($existing);
        $definitionRepo->method('update')->willReturn(false);

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

        $result = $service->updateByUuid('uuid-of-5', [
            'label' => 'Priority',
            'type' => 'text',
            'field_key' => 'priority',
        ], 7);

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

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

        $result = $service->deleteByUuid('');

        $this->assertSame(['ok' => false, 'status' => 404, 'error' => 'not_found'], $result);
    }

    public function testDeleteByUuidRejectsUnknownUuid(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn(null);

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

        $result = $service->deleteByUuid('unknown');

        $this->assertSame(['ok' => false, 'status' => 404, 'error' => 'not_found'], $result);
    }

    public function testDeleteByUuidReturnsFailureWhenRepoReturnsFalse(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn(['id' => 7]);
        $definitionRepo->expects($this->once())->method('delete')->with(7)->willReturn(false);

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

        $result = $service->deleteByUuid('uuid-7');

        $this->assertSame(['ok' => false, 'status' => 500, 'error' => 'delete_failed'], $result);
    }

    public function testDeleteByUuidReturnsDefinitionOnSuccess(): void
    {
        $existing = ['id' => 7, 'tenant_id' => 10, 'label' => 'Gone'];

        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn($existing);
        $definitionRepo->expects($this->once())->method('delete')->with(7)->willReturn(true);

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

        $result = $service->deleteByUuid('uuid-7');

        $this->assertTrue($result['ok']);
        $this->assertSame($existing, $result['definition']);
    }

    public function testDefinitionTenantIdByUuidReturnsZeroWhenNotFound(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->method('findByUuid')->willReturn(null);

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

        $this->assertSame(0, $service->definitionTenantIdByUuid('  '));
    }

    public function testDefinitionTenantIdByUuidReturnsTenantIdWhenFound(): void
    {
        $definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
        $definitionRepo->expects($this->once())->method('findByUuid')->with('abc')->willReturn(['tenant_id' => 42]);

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

        $this->assertSame(42, $service->definitionTenantIdByUuid('abc'));
    }

    private function makeService(
        ?TenantRepository $tenantRepository = null,
        ?TenantCustomFieldDefinitionRepositoryInterface $definitionRepository = null,
        ?TenantCustomFieldOptionRepositoryInterface $optionRepository = null,
    ): TenantCustomFieldService {
        return new TenantCustomFieldService(
            $tenantRepository ?? $this->createMock(TenantRepository::class),
            $definitionRepository ?? $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class),
            $optionRepository ?? $this->createMock(TenantCustomFieldOptionRepositoryInterface::class),
        );
    }
}
