package de.cannamanage.service;

import de.cannamanage.domain.entity.Club;
import de.cannamanage.domain.entity.Distribution;
import de.cannamanage.domain.entity.Member;
import de.cannamanage.domain.entity.StaffAccount;
import de.cannamanage.service.exception.PreventionOfficerLimitExceededException;
import de.cannamanage.service.repository.ClubRepository;
import de.cannamanage.service.repository.DistributionRepository;
import de.cannamanage.service.repository.MemberRepository;
import de.cannamanage.service.repository.StaffAccountRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.server.ResponseStatusException;

import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

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.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class PreventionOfficerServiceTest {

    @Mock private StaffAccountRepository staffAccountRepository;
    @Mock private ClubRepository clubRepository;
    @Mock private MemberRepository memberRepository;
    @Mock private DistributionRepository distributionRepository;

    @InjectMocks private PreventionOfficerService service;

    private UUID tenantId;
    private UUID staffId;
    private StaffAccount staffAccount;
    private Club club;

    @BeforeEach
    void setUp() {
        tenantId = UUID.randomUUID();
        staffId = UUID.randomUUID();

        staffAccount = new StaffAccount();
        staffAccount.setId(staffId);
        staffAccount.setTenantId(tenantId);
        staffAccount.setUserId(UUID.randomUUID());
        staffAccount.setDisplayName("Test Staff");
        staffAccount.setActive(true);
        staffAccount.setPreventionOfficer(false);

        club = new Club();
        club.setId(tenantId);
        club.setTenantId(tenantId);
        club.setMaxPreventionOfficers(2);
    }

    // --- Assignment Tests ---

    @Test
    void assignPreventionOfficer_underLimit_succeeds() {
        when(staffAccountRepository.findById(staffId)).thenReturn(Optional.of(staffAccount));
        when(staffAccountRepository.countByTenantIdAndPreventionOfficerTrueAndActiveTrue(tenantId)).thenReturn(1L);
        when(clubRepository.findById(tenantId)).thenReturn(Optional.of(club));
        when(staffAccountRepository.save(any())).thenAnswer(i -> i.getArgument(0));

        StaffAccount result = service.setPreventionOfficer(tenantId, staffId, true);

        assertThat(result.isPreventionOfficer()).isTrue();
        verify(staffAccountRepository).save(staffAccount);
    }

    @Test
    void assignPreventionOfficer_atLimit_throws409() {
        when(staffAccountRepository.findById(staffId)).thenReturn(Optional.of(staffAccount));
        when(staffAccountRepository.countByTenantIdAndPreventionOfficerTrueAndActiveTrue(tenantId)).thenReturn(2L);
        when(clubRepository.findById(tenantId)).thenReturn(Optional.of(club));

        assertThatThrownBy(() -> service.setPreventionOfficer(tenantId, staffId, true))
                .isInstanceOf(PreventionOfficerLimitExceededException.class)
                .hasMessageContaining("Maximum allowed: 2");

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

    @Test
    void revokePreventionOfficer_alwaysSucceeds() {
        staffAccount.setPreventionOfficer(true);
        when(staffAccountRepository.findById(staffId)).thenReturn(Optional.of(staffAccount));
        when(staffAccountRepository.save(any())).thenAnswer(i -> i.getArgument(0));

        StaffAccount result = service.setPreventionOfficer(tenantId, staffId, false);

        assertThat(result.isPreventionOfficer()).isFalse();
        verify(staffAccountRepository).save(staffAccount);
    }

    @Test
    void assignPreventionOfficer_wrongTenant_throws404() {
        UUID otherTenant = UUID.randomUUID();
        staffAccount.setTenantId(otherTenant);
        when(staffAccountRepository.findById(staffId)).thenReturn(Optional.of(staffAccount));

        assertThatThrownBy(() -> service.setPreventionOfficer(tenantId, staffId, true))
                .isInstanceOf(ResponseStatusException.class)
                .hasMessageContaining("not found");
    }

    @Test
    void assignPreventionOfficer_notFound_throws404() {
        when(staffAccountRepository.findById(staffId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.setPreventionOfficer(tenantId, staffId, true))
                .isInstanceOf(ResponseStatusException.class)
                .hasMessageContaining("not found");
    }

    // --- Under-21 Members ---

    @Test
    void getUnder21Members_returnsList() {
        Member member = new Member();
        member.setId(UUID.randomUUID());
        member.setTenantId(tenantId);
        member.setUnder21(true);
        member.setFirstName("Young");
        member.setLastName("Member");
        member.setDateOfBirth(LocalDate.now().minusYears(20));

        when(memberRepository.findByTenantIdAndUnder21True(tenantId)).thenReturn(List.of(member));

        List<Member> result = service.getUnder21Members(tenantId);

        assertThat(result).hasSize(1);
        assertThat(result.get(0).isUnder21()).isTrue();
    }

    // --- Distribution Counting ---

    @Test
    void countCurrentMonthDistributions_countsCorrectly() {
        UUID memberId = UUID.randomUUID();
        Distribution d1 = new Distribution();
        d1.setMemberId(memberId);
        d1.setDistributedAt(Instant.now());
        d1.setQuantityGrams(BigDecimal.valueOf(5));

        Distribution d2 = new Distribution();
        d2.setMemberId(memberId);
        d2.setDistributedAt(Instant.now());
        d2.setQuantityGrams(BigDecimal.valueOf(10));

        Distribution otherMemberDist = new Distribution();
        otherMemberDist.setMemberId(UUID.randomUUID());
        otherMemberDist.setDistributedAt(Instant.now());
        otherMemberDist.setQuantityGrams(BigDecimal.valueOf(7));

        when(distributionRepository.findByTenantIdAndDistributedAtBetween(eq(tenantId), any(), any()))
                .thenReturn(List.of(d1, d2, otherMemberDist));

        long count = service.countCurrentMonthDistributions(tenantId, memberId);

        assertThat(count).isEqualTo(2);
    }

    @Test
    void sumCurrentMonthGrams_sumsCorrectly() {
        UUID memberId = UUID.randomUUID();
        Distribution d1 = new Distribution();
        d1.setMemberId(memberId);
        d1.setQuantityGrams(BigDecimal.valueOf(5));
        d1.setDistributedAt(Instant.now());

        Distribution d2 = new Distribution();
        d2.setMemberId(memberId);
        d2.setQuantityGrams(BigDecimal.valueOf(10));
        d2.setDistributedAt(Instant.now());

        when(distributionRepository.findByTenantIdAndDistributedAtBetween(eq(tenantId), any(), any()))
                .thenReturn(List.of(d1, d2));

        BigDecimal grams = service.sumCurrentMonthGrams(tenantId, memberId);

        assertThat(grams).isEqualByComparingTo(BigDecimal.valueOf(15));
    }

    // --- Monthly Limit ---

    @Test
    void getMonthlyLimit_under21_returns30() {
        Member member = new Member();
        member.setUnder21(true);

        BigDecimal limit = service.getMonthlyLimit(member);

        assertThat(limit).isEqualByComparingTo(BigDecimal.valueOf(30));
    }

    @Test
    void getMonthlyLimit_adult_returns50() {
        Member member = new Member();
        member.setUnder21(false);

        BigDecimal limit = service.getMonthlyLimit(member);

        assertThat(limit).isEqualByComparingTo(BigDecimal.valueOf(50));
    }

    // --- Age Calculation ---

    @Test
    void calculateAge_returnsCorrectAge() {
        LocalDate dob = LocalDate.now().minusYears(20).minusDays(10);
        int age = service.calculateAge(dob);
        assertThat(age).isEqualTo(20);
    }

    @Test
    void calculateAge_birthday_today() {
        LocalDate dob = LocalDate.now().minusYears(21);
        int age = service.calculateAge(dob);
        assertThat(age).isEqualTo(21);
    }
}
