package at.procon.dip.domain.document.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import at.procon.dip.domain.document.DocumentAttributeValueType;
import at.procon.dip.domain.document.entity.Document;
import at.procon.dip.domain.document.entity.DocumentAttribute;
import at.procon.dip.domain.document.entity.DocumentAttributeName;
import at.procon.dip.domain.document.repository.DocumentAttributeNameRepository;
import at.procon.dip.domain.document.repository.DocumentAttributeRepository;
import at.procon.dip.ingestion.dto.GenericNameValuePairRequest;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class DocumentAttributeServiceTest {

    @Mock
    private DocumentService documentService;
    @Mock
    private DocumentAttributeNameRepository attributeNameRepository;
    @Mock
    private DocumentAttributeRepository attributeRepository;

    private DocumentAttributeService service;

    @BeforeEach
    void setUp() {
        service = new DocumentAttributeService(documentService, attributeNameRepository, attributeRepository);
    }

    @Test
    void shouldCreateTypedCatalogEntriesIncludingIntegerAndDatetimeAndUseContextToAvoidConflicts() {
        UUID documentId = UUID.randomUUID();
        when(documentService.getRequired(documentId)).thenReturn(Document.builder().id(documentId).build());

        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("status", "GLOBAL")).thenReturn(Optional.empty());
        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("status", "TED")).thenReturn(Optional.empty());
        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("estimatedvalue", "GLOBAL")).thenReturn(Optional.empty());
        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("publisheddate", "GLOBAL")).thenReturn(Optional.empty());
        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("framework", "GLOBAL")).thenReturn(Optional.empty());
        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("version", "GLOBAL")).thenReturn(Optional.empty());
        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("lastsyncedat", "GLOBAL")).thenReturn(Optional.empty());
        when(attributeNameRepository.save(any(DocumentAttributeName.class))).thenAnswer(invocation -> {
            DocumentAttributeName value = invocation.getArgument(0);
            value.setId(UUID.randomUUID());
            return value;
        });
        when(attributeRepository.existsByDocument_IdAndAttributeName_IdAndAttributeValueHash(any(), any(), any()))
                .thenReturn(false);

        OffsetDateTime syncedAt = OffsetDateTime.parse("2026-04-21T11:05:00+02:00");

        service.addAttributes(documentId, List.of(
                new GenericNameValuePairRequest("Status", null, DocumentAttributeValueType.STRING, null, "published", null, null, null, null, null),
                new GenericNameValuePairRequest("Status", "ted", DocumentAttributeValueType.STRING, null, "closed", null, null, null, null, null),
                new GenericNameValuePairRequest("estimatedValue", null, DocumentAttributeValueType.NUMBER, null, null, null, new BigDecimal("125000.50"), null, null, null),
                new GenericNameValuePairRequest("publishedDate", null, DocumentAttributeValueType.DATE, null, null, null, null, LocalDate.of(2026, 4, 21), null, null),
                new GenericNameValuePairRequest("framework", null, DocumentAttributeValueType.BOOLEAN, null, null, null, null, null, null, true),
                new GenericNameValuePairRequest("version", null, DocumentAttributeValueType.INTEGER, null, null, 7L, null, null, null, null),
                new GenericNameValuePairRequest("lastSyncedAt", null, DocumentAttributeValueType.DATETIME, null, null, null, null, null, syncedAt, null),
                new GenericNameValuePairRequest("status", null, DocumentAttributeValueType.STRING, null, "published", null, null, null, null, null)
        ));

        verify(attributeNameRepository, times(7)).save(any(DocumentAttributeName.class));
        ArgumentCaptor<DocumentAttribute> captor = ArgumentCaptor.forClass(DocumentAttribute.class);
        verify(attributeRepository, times(7)).save(captor.capture());
        List<DocumentAttribute> saved = captor.getAllValues();

        assertThat(saved).hasSize(7);
        assertThat(saved).anySatisfy(attribute -> {
            assertThat(attribute.getAttributeName().getAttributeContext()).isEqualTo("GLOBAL");
            assertThat(attribute.getAttributeName().getNormalizedName()).isEqualTo("status");
            assertThat(attribute.getStringValue()).isEqualTo("published");
        });
        assertThat(saved).anySatisfy(attribute -> {
            assertThat(attribute.getAttributeName().getAttributeContext()).isEqualTo("TED");
            assertThat(attribute.getAttributeName().getNormalizedName()).isEqualTo("status");
            assertThat(attribute.getStringValue()).isEqualTo("closed");
        });
        assertThat(saved).anySatisfy(attribute -> assertThat(attribute.getNumberValue()).isEqualByComparingTo("125000.5"));
        assertThat(saved).anySatisfy(attribute -> assertThat(attribute.getDateValue()).isEqualTo(LocalDate.of(2026, 4, 21)));
        assertThat(saved).anySatisfy(attribute -> assertThat(attribute.getBooleanValue()).isTrue());
        assertThat(saved).anySatisfy(attribute -> assertThat(attribute.getIntegerValue()).isEqualTo(7L));
        assertThat(saved).anySatisfy(attribute -> assertThat(attribute.getDatetimeValue()).isEqualTo(syncedAt.withNano(0)));
    }

    @Test
    void shouldRejectTypeConflictForExistingCatalogEntryInSameContext() {
        UUID documentId = UUID.randomUUID();
        when(documentService.getRequired(documentId)).thenReturn(Document.builder().id(documentId).build());
        when(attributeNameRepository.findByNormalizedNameAndAttributeContext("status", "TED")).thenReturn(Optional.of(
                DocumentAttributeName.builder()
                        .id(UUID.randomUUID())
                        .attributeName("status")
                        .normalizedName("status")
                        .attributeContext("TED")
                        .attributeValueType(DocumentAttributeValueType.STRING)
                        .build()
        ));

        assertThatThrownBy(() -> service.addAttributes(documentId, List.of(
                new GenericNameValuePairRequest("status", "ted", DocumentAttributeValueType.INTEGER, null, null, 1L, null, null, null, null)
        )))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("already registered as type STRING");

        verify(attributeRepository, times(0)).save(any());
    }
}
