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.verify;
import static org.mockito.Mockito.when;

import de.fete.application.service.exception.EventCancelledException;
import de.fete.application.service.exception.EventExpiredException;
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.model.Rsvp;
import de.fete.domain.model.RsvpToken;
import de.fete.domain.port.out.EventRepository;
import de.fete.domain.port.out.RsvpRepository;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
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 RsvpServiceTest {

  private static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
  private static final Instant NOW = Instant.parse("2026-03-08T12:00:00Z");
  private static final Clock FIXED_CLOCK = Clock.fixed(NOW, ZONE);
  private static final LocalDate TODAY = LocalDate.ofInstant(NOW, ZONE);

  @Mock
  private EventRepository eventRepository;

  @Mock
  private RsvpRepository rsvpRepository;

  private RsvpService rsvpService;

  @BeforeEach
  void setUp() {
    rsvpService = new RsvpService(eventRepository, rsvpRepository, FIXED_CLOCK);
  }

  @Test
  void createRsvpSucceedsForActiveEvent() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
    when(rsvpRepository.save(any(Rsvp.class)))
        .thenAnswer(invocation -> invocation.getArgument(0));

    Rsvp result = rsvpService.createRsvp(token, "Max Mustermann");

    assertThat(result.name()).isEqualTo("Max Mustermann");
    assertThat(result.rsvpToken()).isNotNull();
    assertThat(result.eventId()).isEqualTo(event.id());
  }

  @Test
  void createRsvpPersistsViaRepository() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
    when(rsvpRepository.save(any(Rsvp.class)))
        .thenAnswer(invocation -> invocation.getArgument(0));

    rsvpService.createRsvp(token, "Test Guest");

    ArgumentCaptor<Rsvp> captor = ArgumentCaptor.forClass(Rsvp.class);
    verify(rsvpRepository).save(captor.capture());
    assertThat(captor.getValue().name()).isEqualTo("Test Guest");
    assertThat(captor.getValue().eventId()).isEqualTo(event.id());
  }

  @Test
  void createRsvpThrowsWhenEventNotFound() {
    EventToken token = EventToken.generate();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.empty());

    assertThatThrownBy(() -> rsvpService.createRsvp(token, "Guest"))
        .isInstanceOf(EventNotFoundException.class);
  }

  @Test
  void createRsvpTrimsName() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
    when(rsvpRepository.save(any(Rsvp.class)))
        .thenAnswer(invocation -> invocation.getArgument(0));

    Rsvp result = rsvpService.createRsvp(token, "  Max  ");

    assertThat(result.name()).isEqualTo("Max");
  }

  @Test
  void createRsvpThrowsWhenEventExpired() {
    Event event = buildActiveEvent(TODAY.minusDays(1));
    EventToken token = event.eventToken();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));

    assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
        .isInstanceOf(EventExpiredException.class);
  }

  @Test
  void createRsvpThrowsWhenEventExpiresToday() {
    Event event = buildActiveEvent(TODAY);
    EventToken token = event.eventToken();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));

    assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
        .isInstanceOf(EventExpiredException.class);
  }

  @Test
  void getAttendeeNamesReturnsNamesInOrder() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    OrganizerToken orgToken = event.organizerToken();
    when(eventRepository.findByEventToken(token))
        .thenReturn(Optional.of(event));
    when(rsvpRepository.findByEventId(event.id()))
        .thenReturn(List.of(
            buildRsvp(1L, "Alice"),
            buildRsvp(2L, "Bob"),
            buildRsvp(3L, "Charlie")));

    List<String> names = rsvpService.getAttendeeNames(token, orgToken);

    assertThat(names).containsExactly("Alice", "Bob", "Charlie");
  }

  @Test
  void getAttendeeNamesReturnsEmptyListWhenNoRsvps() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    OrganizerToken orgToken = event.organizerToken();
    when(eventRepository.findByEventToken(token))
        .thenReturn(Optional.of(event));
    when(rsvpRepository.findByEventId(event.id()))
        .thenReturn(List.of());

    List<String> names = rsvpService.getAttendeeNames(token, orgToken);

    assertThat(names).isEmpty();
  }

  @Test
  void getAttendeeNamesThrowsWhenEventNotFound() {
    EventToken token = EventToken.generate();
    OrganizerToken orgToken = OrganizerToken.generate();
    when(eventRepository.findByEventToken(token))
        .thenReturn(Optional.empty());

    assertThatThrownBy(
        () -> rsvpService.getAttendeeNames(token, orgToken))
        .isInstanceOf(EventNotFoundException.class);
  }

  @Test
  void getAttendeeNamesThrowsWhenOrganizerTokenInvalid() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    OrganizerToken wrongToken = OrganizerToken.generate();
    when(eventRepository.findByEventToken(token))
        .thenReturn(Optional.of(event));

    assertThatThrownBy(
        () -> rsvpService.getAttendeeNames(token, wrongToken))
        .isInstanceOf(InvalidOrganizerTokenException.class);
  }

  private Rsvp buildRsvp(Long id, String name) {
    return new Rsvp(id, RsvpToken.generate(), 1L, name);
  }

  @Test
  void cancelRsvpDeletesWhenEventAndRsvpExist() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    RsvpToken rsvpToken = RsvpToken.generate();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
    when(rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken)).thenReturn(true);

    rsvpService.cancelRsvp(token, rsvpToken);

    verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
  }

  @Test
  void cancelRsvpSucceedsWhenRsvpNotFound() {
    Event event = buildActiveEvent(TODAY.plusDays(30));
    EventToken token = event.eventToken();
    RsvpToken rsvpToken = RsvpToken.generate();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
    when(rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken)).thenReturn(false);

    rsvpService.cancelRsvp(token, rsvpToken);

    verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
  }

  @Test
  void cancelRsvpSucceedsWhenEventNotFound() {
    EventToken token = EventToken.generate();
    RsvpToken rsvpToken = RsvpToken.generate();
    when(eventRepository.findByEventToken(token)).thenReturn(Optional.empty());

    rsvpService.cancelRsvp(token, rsvpToken);
  }

  private Event buildActiveEvent(LocalDate expiryDate) {
    return new Event(
        1L,
        EventToken.generate(),
        OrganizerToken.generate(),
        "Test Event",
        null,
        OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)),
        ZONE,
        null,
        expiryDate,
        OffsetDateTime.now(),
        false,
        null);
  }
}
