package org.springblade.modules.martial;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springblade.modules.martial.mapper.*;
import org.springblade.modules.martial.pojo.entity.*;
import org.springblade.modules.martial.service.*;
import org.springblade.modules.martial.service.impl.MartialSchedulePlanServiceImpl;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;

import static org.junit.jupiter.api.Assertions.*;

/**
 * MartialSchedulePlanServiceImpl Unit Tests - Google Standard
 * Tests scheduling logic: time slots, conflict detection, venue allocation
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("MartialSchedulePlanServiceImpl Scheduling Logic Tests")
class MartialSchedulePlanServiceImplTest {

    @InjectMocks
    private MartialSchedulePlanServiceImpl schedulePlanService;

    @Mock
    private MartialScheduleSlotMapper slotMapper;
    @Mock
    private MartialScheduleAthleteSlotMapper athleteSlotMapper;
    @Mock
    private MartialScheduleConflictMapper conflictMapper;
    @Mock
    private MartialScheduleAdjustmentLogMapper adjustmentLogMapper;
    @Mock
    private IMartialCompetitionService competitionService;
    @Mock
    private IMartialProjectService projectService;
    @Mock
    private IMartialVenueService venueService;
    @Mock
    private IMartialAthleteService athleteService;
    @Mock
    private IMartialRegistrationOrderService registrationOrderService;

    // ==================== Time Slot Generation Tests ====================

    @Nested
    @DisplayName("generateTimeSlots() - Time Slot Logic")
    class TimeSlotGenerationTests {

        @Test
        @DisplayName("generate 30-minute slots for 2-hour period")
        void generateTimeSlots_2hours_4slots() {
            LocalDateTime start = LocalDateTime.of(2026, 1, 16, 9, 0);
            LocalDateTime end = LocalDateTime.of(2026, 1, 16, 11, 0);
            int durationMinutes = 30;
            
            // Calculate expected slots
            long totalMinutes = java.time.Duration.between(start, end).toMinutes();
            int expectedSlots = (int) (totalMinutes / durationMinutes);
            
            assertEquals(4, expectedSlots);
        }

        @Test
        @DisplayName("generate 15-minute slots for 1-hour period")
        void generateTimeSlots_1hour_15min_4slots() {
            LocalDateTime start = LocalDateTime.of(2026, 1, 16, 9, 0);
            LocalDateTime end = LocalDateTime.of(2026, 1, 16, 10, 0);
            int durationMinutes = 15;
            
            long totalMinutes = java.time.Duration.between(start, end).toMinutes();
            int expectedSlots = (int) (totalMinutes / durationMinutes);
            
            assertEquals(4, expectedSlots);
        }

        @Test
        @DisplayName("partial slot at end is not created")
        void generateTimeSlots_partialSlot_notCreated() {
            LocalDateTime start = LocalDateTime.of(2026, 1, 16, 9, 0);
            LocalDateTime end = LocalDateTime.of(2026, 1, 16, 9, 45); // 45 minutes
            int durationMinutes = 30;
            
            long totalMinutes = java.time.Duration.between(start, end).toMinutes();
            int expectedSlots = (int) (totalMinutes / durationMinutes);
            
            assertEquals(1, expectedSlots); // Only 1 full 30-min slot
        }
    }

    // ==================== Time Overlap Detection Tests ====================

    @Nested
    @DisplayName("timeOverlaps() - Overlap Detection")
    class TimeOverlapTests {

        @Test
        @DisplayName("overlapping times detected")
        void timeOverlaps_overlapping_true() {
            LocalTime start1 = LocalTime.of(9, 0);
            LocalTime end1 = LocalTime.of(10, 0);
            LocalTime start2 = LocalTime.of(9, 30);
            LocalTime end2 = LocalTime.of(10, 30);
            
            boolean overlaps = timeOverlaps(start1, end1, start2, end2);
            assertTrue(overlaps);
        }

        @Test
        @DisplayName("non-overlapping times not detected")
        void timeOverlaps_nonOverlapping_false() {
            LocalTime start1 = LocalTime.of(9, 0);
            LocalTime end1 = LocalTime.of(10, 0);
            LocalTime start2 = LocalTime.of(10, 30);
            LocalTime end2 = LocalTime.of(11, 30);
            
            boolean overlaps = timeOverlaps(start1, end1, start2, end2);
            assertFalse(overlaps);
        }

        @Test
        @DisplayName("adjacent times (end = start) not overlapping")
        void timeOverlaps_adjacent_false() {
            LocalTime start1 = LocalTime.of(9, 0);
            LocalTime end1 = LocalTime.of(10, 0);
            LocalTime start2 = LocalTime.of(10, 0);
            LocalTime end2 = LocalTime.of(11, 0);
            
            boolean overlaps = timeOverlaps(start1, end1, start2, end2);
            assertFalse(overlaps);
        }

        @Test
        @DisplayName("contained time range overlaps")
        void timeOverlaps_contained_true() {
            LocalTime start1 = LocalTime.of(9, 0);
            LocalTime end1 = LocalTime.of(12, 0);
            LocalTime start2 = LocalTime.of(10, 0);
            LocalTime end2 = LocalTime.of(11, 0);
            
            boolean overlaps = timeOverlaps(start1, end1, start2, end2);
            assertTrue(overlaps);
        }

        private boolean timeOverlaps(LocalTime start1, LocalTime end1, LocalTime start2, LocalTime end2) {
            return start1.isBefore(end2) && start2.isBefore(end1);
        }
    }

    // ==================== Project Sorting Tests ====================

    @Nested
    @DisplayName("Project Sorting - Group Events First")
    class ProjectSortingTests {

        @Test
        @DisplayName("group projects (type=3) sorted before individual (type=1)")
        void projectSort_groupBeforeIndividual() {
            List<MartialProject> projects = Arrays.asList(
                createProject(1L, "Individual A", 1),
                createProject(2L, "Group B", 3),
                createProject(3L, "Pair C", 2)
            );
            
            // Sort: type descending (3 > 2 > 1)
            projects.sort((a, b) -> {
                Integer typeA = a.getType() != null ? a.getType() : 1;
                Integer typeB = b.getType() != null ? b.getType() : 1;
                return typeB.compareTo(typeA);
            });
            
            assertEquals(3, projects.get(0).getType()); // Group first
            assertEquals(2, projects.get(1).getType()); // Pair second
            assertEquals(1, projects.get(2).getType()); // Individual last
        }

        @Test
        @DisplayName("same type sorted by name")
        void projectSort_sameType_byName() {
            List<MartialProject> projects = Arrays.asList(
                createProject(1L, "Taichi", 1),
                createProject(2L, "Changquan", 1),
                createProject(3L, "Nanquan", 1)
            );
            
            projects.sort((a, b) -> {
                Integer typeA = a.getType() != null ? a.getType() : 1;
                Integer typeB = b.getType() != null ? b.getType() : 1;
                if (!typeA.equals(typeB)) {
                    return typeB.compareTo(typeA);
                }
                return a.getProjectName().compareTo(b.getProjectName());
            });
            
            assertEquals("Changquan", projects.get(0).getProjectName());
            assertEquals("Nanquan", projects.get(1).getProjectName());
            assertEquals("Taichi", projects.get(2).getProjectName());
        }

        private MartialProject createProject(Long id, String name, Integer type) {
            MartialProject p = new MartialProject();
            p.setId(id);
            p.setProjectName(name);
            p.setType(type);
            return p;
        }
    }

    // ==================== Slot Calculation Tests ====================

    @Nested
    @DisplayName("Slot Calculation Logic")
    class SlotCalculationTests {

        @ParameterizedTest
        @CsvSource({
            "10, 10, 30, 4",   // 10 athletes * 10 min = 100 min, needs 4 slots (30 min each)
            "5, 10, 30, 2",    // 5 athletes * 10 min = 50 min, needs 2 slots
            "3, 10, 30, 1",    // 3 athletes * 10 min = 30 min, needs 1 slot
            "1, 10, 30, 1",    // 1 athlete * 10 min = 10 min, needs 1 slot
            "10, 5, 30, 2"     // 10 athletes * 5 min = 50 min, needs 2 slots
        })
        @DisplayName("calculate slots needed for athletes")
        void calculateSlotsNeeded(int athleteCount, int slotDuration, int slotSize, int expectedSlots) {
            int slotsNeeded = (int) Math.ceil((double) (athleteCount * slotDuration) / slotSize);
            assertEquals(expectedSlots, slotsNeeded);
        }
    }

    // ==================== Conflict Detection Tests ====================

    @Nested
    @DisplayName("Conflict Detection Logic")
    class ConflictDetectionTests {

        @Test
        @DisplayName("athlete in two venues at same time is conflict")
        void conflict_athleteTwoVenues_detected() {
            // Athlete 1 scheduled at Venue A 9:00-9:30 and Venue B 9:15-9:45
            LocalTime start1 = LocalTime.of(9, 0);
            LocalTime end1 = LocalTime.of(9, 30);
            LocalTime start2 = LocalTime.of(9, 15);
            LocalTime end2 = LocalTime.of(9, 45);
            
            boolean overlaps = start1.isBefore(end2) && start2.isBefore(end1);
            assertTrue(overlaps, "Should detect time conflict");
        }

        @Test
        @DisplayName("athlete in two venues at different times is not conflict")
        void conflict_athleteDifferentTimes_notDetected() {
            LocalTime start1 = LocalTime.of(9, 0);
            LocalTime end1 = LocalTime.of(9, 30);
            LocalTime start2 = LocalTime.of(10, 0);
            LocalTime end2 = LocalTime.of(10, 30);
            
            boolean overlaps = start1.isBefore(end2) && start2.isBefore(end1);
            assertFalse(overlaps, "Should not detect conflict");
        }

        @Test
        @DisplayName("venue double-booked is conflict")
        void conflict_venueDoubleBooked_detected() {
            // Venue A has Project 1 at 9:00-10:00 and Project 2 at 9:30-10:30
            LocalTime start1 = LocalTime.of(9, 0);
            LocalTime end1 = LocalTime.of(10, 0);
            LocalTime start2 = LocalTime.of(9, 30);
            LocalTime end2 = LocalTime.of(10, 30);
            
            boolean overlaps = start1.isBefore(end2) && start2.isBefore(end1);
            assertTrue(overlaps, "Should detect venue conflict");
        }
    }

    // ==================== Plan Status Tests ====================

    @Nested
    @DisplayName("Plan Status Transitions")
    class PlanStatusTests {

        @Test
        @DisplayName("status 0 = draft")
        void status_0_draft() {
            MartialSchedulePlan plan = new MartialSchedulePlan();
            plan.setStatus(0);
            assertEquals(0, plan.getStatus());
        }

        @Test
        @DisplayName("status 1 = confirmed")
        void status_1_confirmed() {
            MartialSchedulePlan plan = new MartialSchedulePlan();
            plan.setStatus(1);
            assertEquals(1, plan.getStatus());
        }

        @Test
        @DisplayName("status 2 = published")
        void status_2_published() {
            MartialSchedulePlan plan = new MartialSchedulePlan();
            plan.setStatus(2);
            assertEquals(2, plan.getStatus());
        }

        @Test
        @DisplayName("cannot publish plan with conflicts")
        void publish_withConflicts_shouldFail() {
            MartialSchedulePlan plan = new MartialSchedulePlan();
            plan.setConflictCount(3);
            
            // Business rule: cannot publish if conflictCount > 0
            assertTrue(plan.getConflictCount() > 0, "Should not allow publish with conflicts");
        }
    }
}
