package com.codebyte.api.item.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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.codebyte.api.common.web.PagedResponse;
import com.codebyte.api.item.api.dto.CreateItemRequest;
import com.codebyte.api.item.api.dto.ItemResponse;
import com.codebyte.api.item.api.dto.PatchItemRequest;
import com.codebyte.api.item.api.dto.UpdateItemRequest;
import com.codebyte.api.item.domain.Item;
import com.codebyte.api.item.mapper.ItemMapper;
import com.codebyte.api.item.repository.ItemRepository;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openapitools.jackson.nullable.JsonNullable;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

@ExtendWith(MockitoExtension.class)
class ItemServiceImplTest {

    @Mock private ItemRepository repository;
    @Mock private ItemMapper mapper;
    @InjectMocks private ItemServiceImpl service;

    private static Item item(UUID id, String name, long version) {
        Item item = new Item(name, "desc");
        item.setId(id);
        item.setVersion(version);
        return item;
    }

    private static ItemResponse response(UUID id, String name) {
        return new ItemResponse(id, name, "desc", Instant.now(), Instant.now(), 0L);
    }

    // --- create ---

    @Test
    void should_create_item_when_name_is_free() {
        CreateItemRequest request = new CreateItemRequest("Widget", "desc");
        Item entity = item(null, "Widget", 0L);
        Item saved = item(UUID.randomUUID(), "Widget", 0L);
        ItemResponse expected = response(saved.getId(), "Widget");
        when(repository.existsByNameIgnoreCase("Widget")).thenReturn(false);
        when(mapper.toEntity(request)).thenReturn(entity);
        when(repository.saveAndFlush(entity)).thenReturn(saved);
        when(mapper.toResponse(saved)).thenReturn(expected);

        assertThat(service.create(request)).isEqualTo(expected);
    }

    @Test
    void should_reject_create_when_name_already_taken() {
        CreateItemRequest request = new CreateItemRequest("Widget", "desc");
        when(repository.existsByNameIgnoreCase("Widget")).thenReturn(true);

        assertThatThrownBy(() -> service.create(request))
                .isInstanceOf(DuplicateItemNameException.class);
        verify(repository, never()).saveAndFlush(any());
    }

    // --- get ---

    @Test
    void should_return_item_when_found() {
        UUID id = UUID.randomUUID();
        Item found = item(id, "Widget", 0L);
        ItemResponse expected = response(id, "Widget");
        when(repository.findById(id)).thenReturn(Optional.of(found));
        when(mapper.toResponse(found)).thenReturn(expected);

        assertThat(service.get(id)).isEqualTo(expected);
    }

    @Test
    void should_throw_not_found_when_getting_missing_item() {
        UUID id = UUID.randomUUID();
        when(repository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.get(id)).isInstanceOf(ItemNotFoundException.class);
    }

    // --- list ---

    @Test
    void should_list_all_when_query_is_null() {
        Pageable pageable = PageRequest.of(0, 20);
        Item found = item(UUID.randomUUID(), "Widget", 0L);
        when(repository.findAll(pageable)).thenReturn(new PageImpl<>(List.of(found)));
        when(mapper.toResponse(found)).thenReturn(response(found.getId(), "Widget"));

        PagedResponse<ItemResponse> result = service.list(null, pageable);

        assertThat(result.content()).hasSize(1);
        assertThat(result.totalElements()).isEqualTo(1);
    }

    @Test
    void should_list_all_when_query_is_blank() {
        Pageable pageable = PageRequest.of(0, 20);
        when(repository.findAll(pageable)).thenReturn(new PageImpl<>(List.of()));

        assertThat(service.list("   ", pageable).content()).isEmpty();
    }

    @Test
    void should_search_when_query_present() {
        Pageable pageable = PageRequest.of(0, 20);
        Item found = item(UUID.randomUUID(), "Widget", 0L);
        when(repository.findByNameContainingIgnoreCaseOrDescriptionContainingIgnoreCase(
                        "wid", "wid", pageable))
                .thenReturn(new PageImpl<>(List.of(found)));
        when(mapper.toResponse(found)).thenReturn(response(found.getId(), "Widget"));

        assertThat(service.list("  wid  ", pageable).content()).hasSize(1);
    }

    // --- replace ---

    @Test
    void should_replace_when_version_matches_and_name_unchanged() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 3L);
        UpdateItemRequest request = new UpdateItemRequest("Widget", "new", 3L);
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.saveAndFlush(existing)).thenReturn(existing);
        when(mapper.toResponse(existing)).thenReturn(response(id, "Widget"));

        service.replace(id, request);

        verify(mapper).updateEntity(request, existing);
        verify(repository, never()).existsByNameIgnoreCase(any());
    }

    @Test
    void should_reject_replace_when_version_is_stale() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 5L);
        UpdateItemRequest request = new UpdateItemRequest("Widget", "new", 4L);
        when(repository.findById(id)).thenReturn(Optional.of(existing));

        assertThatThrownBy(() -> service.replace(id, request))
                .isInstanceOf(ItemVersionConflictException.class);
        verify(repository, never()).saveAndFlush(any());
    }

    @Test
    void should_reject_replace_when_new_name_taken() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        UpdateItemRequest request = new UpdateItemRequest("Gadget", "new", 0L);
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.existsByNameIgnoreCase("Gadget")).thenReturn(true);

        assertThatThrownBy(() -> service.replace(id, request))
                .isInstanceOf(DuplicateItemNameException.class);
    }

    @Test
    void should_replace_when_new_name_is_free() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        UpdateItemRequest request = new UpdateItemRequest("Gadget", "new", 0L);
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.existsByNameIgnoreCase("Gadget")).thenReturn(false);
        when(repository.saveAndFlush(existing)).thenReturn(existing);
        when(mapper.toResponse(existing)).thenReturn(response(id, "Gadget"));

        assertThat(service.replace(id, request)).isNotNull();
        verify(mapper).updateEntity(request, existing);
    }

    // --- patch ---

    @Test
    void should_patch_name_and_description_when_present() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        PatchItemRequest request =
                new PatchItemRequest(JsonNullable.of("Gadget"), JsonNullable.of("changed"));
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.existsByNameIgnoreCase("Gadget")).thenReturn(false);
        when(repository.saveAndFlush(existing)).thenReturn(existing);
        when(mapper.toResponse(existing)).thenReturn(response(id, "Gadget"));

        service.patch(id, request);

        assertThat(existing.getName()).isEqualTo("Gadget");
        assertThat(existing.getDescription()).isEqualTo("changed");
    }

    @Test
    void should_clear_description_when_patched_with_explicit_null() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        PatchItemRequest request =
                new PatchItemRequest(JsonNullable.undefined(), JsonNullable.of(null));
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.saveAndFlush(existing)).thenReturn(existing);
        when(mapper.toResponse(existing)).thenReturn(response(id, "Widget"));

        service.patch(id, request);

        assertThat(existing.getName()).isEqualTo("Widget");
        assertThat(existing.getDescription()).isNull();
    }

    @Test
    void should_leave_fields_unchanged_when_patch_absent() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        PatchItemRequest request =
                new PatchItemRequest(JsonNullable.undefined(), JsonNullable.undefined());
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.saveAndFlush(existing)).thenReturn(existing);
        when(mapper.toResponse(existing)).thenReturn(response(id, "Widget"));

        service.patch(id, request);

        assertThat(existing.getName()).isEqualTo("Widget");
        assertThat(existing.getDescription()).isEqualTo("desc");
        verify(repository, never()).existsByNameIgnoreCase(any());
    }

    @Test
    void should_reject_patch_when_new_name_taken() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        PatchItemRequest request =
                new PatchItemRequest(JsonNullable.of("Gadget"), JsonNullable.undefined());
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.existsByNameIgnoreCase("Gadget")).thenReturn(true);

        assertThatThrownBy(() -> service.patch(id, request))
                .isInstanceOf(DuplicateItemNameException.class);
    }

    @Test
    void should_keep_same_name_when_patched_to_current_name_ignoring_case() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        PatchItemRequest request =
                new PatchItemRequest(JsonNullable.of("widget"), JsonNullable.undefined());
        when(repository.findById(id)).thenReturn(Optional.of(existing));
        when(repository.saveAndFlush(existing)).thenReturn(existing);
        when(mapper.toResponse(existing)).thenReturn(response(id, "widget"));

        service.patch(id, request);

        assertThat(existing.getName()).isEqualTo("widget");
        verify(repository, never()).existsByNameIgnoreCase(any());
    }

    // --- delete ---

    @Test
    void should_delete_when_item_exists() {
        UUID id = UUID.randomUUID();
        Item existing = item(id, "Widget", 0L);
        when(repository.findById(id)).thenReturn(Optional.of(existing));

        service.delete(id);

        ArgumentCaptor<Item> captor = ArgumentCaptor.forClass(Item.class);
        verify(repository).delete(captor.capture());
        assertThat(captor.getValue()).isEqualTo(existing);
    }

    @Test
    void should_be_idempotent_when_deleting_missing_item() {
        UUID id = UUID.randomUUID();
        when(repository.findById(id)).thenReturn(Optional.empty());

        service.delete(id);

        verify(repository, never()).delete(any());
    }
}
