package org.springblade.modules.martial;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.modules.martial.mapper.MartialResultMapper;
import org.springblade.modules.martial.pojo.entity.MartialProject;
import org.springblade.modules.martial.pojo.entity.MartialScore;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialCompetitionService;
import org.springblade.modules.martial.service.IMartialProjectService;
import org.springblade.modules.martial.service.IMartialScoreService;
import org.springblade.modules.martial.service.impl.MartialResultServiceImpl;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * MartialResultServiceImpl Tests - OpenAI Standard
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("OpenAI Standard: MartialResultServiceImpl Tests")
class MartialResultServiceOpenAITest {

    @Spy
    @InjectMocks
    private MartialResultServiceImpl resultService;

    @Mock
    private MartialResultMapper resultMapper;
    @Mock
    private IMartialScoreService scoreService;
    @Mock
    private IMartialAthleteService athleteService;
    @Mock
    private IMartialProjectService projectService;
    @Mock
    private IMartialCompetitionService competitionService;

    // ========== Property-Based Testing ==========

    @Nested
    @DisplayName("Property-Based Testing")
    class PropertyBasedTests {

        @ParameterizedTest
        @MethodSource("org.springblade.modules.martial.MartialResultServiceOpenAITest#randomScoreSets")
        @DisplayName("Property: Average always between min and max of input scores")
        void property_calculateAverage_resultBetweenMinMax(List<BigDecimal> scores) {
            when(scoreService.list(any(QueryWrapper.class))).thenReturn(
                scores.stream().map(s -> buildScore(s)).toList()
            );
            
            BigDecimal result = resultService.calculateValidAverageScore(1L, 10L);
            BigDecimal min = scores.stream().min(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
            BigDecimal max = scores.stream().max(Comparator.naturalOrder()).orElse(BigDecimal.TEN);
            
            assertTrue(result.compareTo(min) >= 0, "Result should be >= min score");
            assertTrue(result.compareTo(max) <= 0, "Result should be <= max score");
        }
    }

    // ========== Invariant Testing ==========

    @Nested
    @DisplayName("Invariant Testing")
    class InvariantTests {

        @Test
        @DisplayName("Invariant: Removing max/min reduces count by exactly 2")
        void invariant_calculateAverage_removesExactlyTwo() {
            List<MartialScore> scores = Arrays.asList(
                buildScore(new BigDecimal("9.0")),
                buildScore(new BigDecimal("9.2")),
                buildScore(new BigDecimal("9.5")),
                buildScore(new BigDecimal("8.8")),
                buildScore(new BigDecimal("9.1"))
            );
            when(scoreService.list(any(QueryWrapper.class))).thenReturn(scores);
            
            // 5 scores - 2 (max+min) = 3 scores for average
            BigDecimal result = resultService.calculateValidAverageScore(1L, 10L);
            assertNotNull(result);
        }

        @Test
        @DisplayName("Invariant: Coefficient multiplier is always applied")
        void invariant_applyCoefficient_alwaysMultiplied() {
            MartialProject project = new MartialProject();
            project.setId(10L);
            project.setDifficultyCoefficient(new BigDecimal("1.20"));
            when(projectService.getById(10L)).thenReturn(project);
            
            BigDecimal input = new BigDecimal("9.000");
            BigDecimal result = resultService.applyDifficultyCoefficient(input, 10L);
            
            // Invariant: result = input * coefficient
            assertEquals(input.multiply(new BigDecimal("1.20")).setScale(3, RoundingMode.HALF_UP), result);
        }
    }

    // ========== Regression Testing ==========

    @Nested
    @DisplayName("Regression Testing")
    class RegressionTests {

        @Test
        @Tag("regression")
        @Tag("BUG-2024-003")
        @DisplayName("Regression: Less than 3 scores should throw exception")
        void regression_calculateAverage_lessThan3Scores() {
            when(scoreService.list(any(QueryWrapper.class))).thenReturn(Arrays.asList(
                buildScore(new BigDecimal("9.0")),
                buildScore(new BigDecimal("9.5"))
            ));
            
            ServiceException ex = assertThrows(ServiceException.class,
                () -> resultService.calculateValidAverageScore(1L, 10L));
            assertEquals("裁判人数不足3人，无法去最高/最低分", ex.getMessage());
        }

        @Test
        @Tag("regression")
        @Tag("BUG-2024-004")
        @DisplayName("Regression: Null coefficient should default to 1.0")
        void regression_applyCoefficient_nullDefault() {
            MartialProject project = new MartialProject();
            project.setId(10L);
            project.setDifficultyCoefficient(null);
            when(projectService.getById(10L)).thenReturn(project);
            
            BigDecimal result = resultService.applyDifficultyCoefficient(new BigDecimal("9.000"), 10L);
            assertEquals(new BigDecimal("9.000"), result);
        }
    }

    // ========== Contract Testing ==========

    @Nested
    @DisplayName("Contract Testing")
    class ContractTests {

        @Test
        @DisplayName("Contract: calculateValidAverageScore returns BigDecimal")
        void contract_calculateAverage_returnType() {
            when(scoreService.list(any(QueryWrapper.class))).thenReturn(Arrays.asList(
                buildScore(new BigDecimal("9.0")),
                buildScore(new BigDecimal("9.2")),
                buildScore(new BigDecimal("9.5"))
            ));
            
            Object result = resultService.calculateValidAverageScore(1L, 10L);
            assertInstanceOf(BigDecimal.class, result);
        }

        @Test
        @DisplayName("Contract: applyDifficultyCoefficient with non-existent project throws")
        void contract_applyCoefficient_notFound_throws() {
            when(projectService.getById(anyLong())).thenReturn(null);
            
            Exception ex = assertThrows(Exception.class,
                () -> resultService.applyDifficultyCoefficient(new BigDecimal("9.0"), 999L));
            assertInstanceOf(ServiceException.class, ex);
        }
    }

    // ========== Helper Methods ==========

    static Stream<List<BigDecimal>> randomScoreSets() {
        Random random = new Random(42);
        return Stream.generate(() -> {
            int size = 3 + random.nextInt(5); // 3-7 scores
            List<BigDecimal> scores = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                scores.add(new BigDecimal(5 + random.nextDouble() * 5).setScale(3, RoundingMode.HALF_UP));
            }
            return scores;
        }).limit(20);
    }

    private MartialScore buildScore(BigDecimal score) {
        MartialScore s = new MartialScore();
        s.setId((long)(Math.random() * 1000));
        s.setJudgeId((long)(Math.random() * 1000));
        s.setAthleteId(100L);
        s.setProjectId(10L);
        s.setScore(score);
        return s;
    }
}
