package de.fete.application.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 de.fete.application.service.exception.EventAlreadyCancelledException;
import de.fete.application.service.exception.EventNotFoundException;
import de.fete.application.service.exception.InvalidOrganizerTokenException;
import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken;
import de.fete.domain.model.OrganizerToken;
import de.fete.domain.port.out.EventRepository;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Optional;
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 EventServiceCancelTest {

  private static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
  private static final Instant FIXED_INSTANT =
      LocalDate.of(2026, 3, 5).atStartOfDay(ZONE).toInstant();
  private static final Clock FIXED_CLOCK = Clock.fixed(FIXED_INSTANT, ZONE);

  @Mock
  private EventRepository eventRepository;

  private EventService eventService;

  @BeforeEach
  void setUp() {
    eventService = new EventService(eventRepository, FIXED_CLOCK);
  }

  @Test
  void cancelEventDelegatesToDomainAndSaves() {
    EventToken eventToken = EventToken.generate();
    OrganizerToken organizerToken = OrganizerToken.generate();
    var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
        null, false, null);

    when(eventRepository.findByEventToken(eventToken))
        .thenReturn(Optional.of(event));
    when(eventRepository.save(any(Event.class)))
        .thenAnswer(invocation -> invocation.getArgument(0));

    eventService.cancelEvent(eventToken, organizerToken, true, "Venue unavailable");

    ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
    verify(eventRepository).save(captor.capture());
    assertThat(captor.getValue().cancelled()).isTrue();
    assertThat(captor.getValue().cancellationReason()).isEqualTo("Venue unavailable");
  }

  @Test
  void cancelEventWithNullReason() {
    EventToken eventToken = EventToken.generate();
    OrganizerToken organizerToken = OrganizerToken.generate();
    var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
        null, false, null);

    when(eventRepository.findByEventToken(eventToken))
        .thenReturn(Optional.of(event));
    when(eventRepository.save(any(Event.class)))
        .thenAnswer(invocation -> invocation.getArgument(0));

    eventService.cancelEvent(eventToken, organizerToken, true, null);

    ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
    verify(eventRepository).save(captor.capture());
    assertThat(captor.getValue().cancelled()).isTrue();
    assertThat(captor.getValue().cancellationReason()).isNull();
  }

  @Test
  void cancelEventThrows404WhenNotFound() {
    EventToken eventToken = EventToken.generate();
    OrganizerToken organizerToken = OrganizerToken.generate();

    when(eventRepository.findByEventToken(eventToken))
        .thenReturn(Optional.empty());

    assertThatThrownBy(() -> eventService.cancelEvent(eventToken, organizerToken, true, null))
        .isInstanceOf(EventNotFoundException.class);

    verify(eventRepository, never()).save(any());
  }

  @Test
  void cancelEventThrows403WhenWrongOrganizerToken() {
    EventToken eventToken = EventToken.generate();
    OrganizerToken correctToken = OrganizerToken.generate();
    var event = new Event(null, eventToken, correctToken, null, null, null, null, null, null,
        null, false, null);

    when(eventRepository.findByEventToken(eventToken))
        .thenReturn(Optional.of(event));

    final OrganizerToken wrongToken = OrganizerToken.generate();
    assertThatThrownBy(() -> eventService.cancelEvent(eventToken, wrongToken, true, null))
        .isInstanceOf(InvalidOrganizerTokenException.class);

    verify(eventRepository, never()).save(any());
  }

  @Test
  void cancelEventThrows409WhenAlreadyCancelled() {
    EventToken eventToken = EventToken.generate();
    OrganizerToken organizerToken = OrganizerToken.generate();
    var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
        null, true, null);

    when(eventRepository.findByEventToken(eventToken))
        .thenReturn(Optional.of(event));

    assertThatThrownBy(() -> eventService.cancelEvent(eventToken, organizerToken, true, null))
        .isInstanceOf(EventAlreadyCancelledException.class);

    verify(eventRepository, never()).save(any());
  }
}
