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.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springblade.modules.martial.pojo.entity.MartialScore;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

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

/**
 * Enhanced MartialScoreService Unit Tests
 * @author QA Team
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("Score Service Enhanced Tests")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MartialScoreCalculationTest {

    private MartialScore validScore;

    @BeforeEach
    void setUp() {
        validScore = createValidScore();
    }

    private MartialScore createValidScore() {
        MartialScore score = new MartialScore();
        score.setId(1L);
        score.setCompetitionId(1L);
        score.setScheduleId(100L);
        score.setProjectId(10L);
        score.setAthleteId(1L);
        score.setJudgeId(5L);
        score.setJudgeName("Judge Zhang");
        score.setScore(new BigDecimal("9.50"));
        score.setOriginalScore(new BigDecimal("9.50"));
        score.setScoreTime(LocalDateTime.now());
        score.setStatus(1);
        return score;
    }

    @Nested
    @DisplayName("Score Calculation Tests")
    class ScoreCalculationTests {
        @Test
        @DisplayName("Calculate final score without deduction")
        void test_calculateFinalScore_noDeduction() {
            BigDecimal baseScore = new BigDecimal("9.50");
            BigDecimal deduction = BigDecimal.ZERO;
            BigDecimal expected = new BigDecimal("9.50");
            assertEquals(expected, baseScore.subtract(deduction));
        }

        @Test
        @DisplayName("Calculate final score with deduction")
        void test_calculateFinalScore_withDeduction() {
            BigDecimal baseScore = new BigDecimal("9.50");
            BigDecimal deduction = new BigDecimal("0.30");
            BigDecimal expected = new BigDecimal("9.20");
            assertEquals(expected, baseScore.subtract(deduction));
        }

        @ParameterizedTest
        @CsvSource({"9.50, 0.00, 1.00, 9.50", "9.00, 0.20, 1.00, 8.80", "8.50, 0.10, 1.20, 10.08", "9.00, 0.00, 0.80, 7.20"})
        @DisplayName("Calculate final score with difficulty coefficient")
        void test_calculateFinalScore_withCoefficient(String base, String deduct, String coeff, String expected) {
            BigDecimal baseScore = new BigDecimal(base);
            BigDecimal deduction = new BigDecimal(deduct);
            BigDecimal coefficient = new BigDecimal(coeff);
            BigDecimal expectedScore = new BigDecimal(expected);
            BigDecimal actual = baseScore.subtract(deduction).multiply(coefficient).setScale(2, RoundingMode.HALF_UP);
            assertEquals(expectedScore, actual);
        }

        @Test
        @DisplayName("Calculate average score from multiple judges")
        void test_calculateAverageScore() {
            List<BigDecimal> scores = Arrays.asList(
                new BigDecimal("9.50"), new BigDecimal("9.30"), new BigDecimal("9.40"),
                new BigDecimal("9.60"), new BigDecimal("9.20")
            );
            BigDecimal sum = scores.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal average = sum.divide(BigDecimal.valueOf(scores.size()), 2, RoundingMode.HALF_UP);
            assertEquals(new BigDecimal("9.40"), average);
        }

        @Test
        @DisplayName("Calculate average removing highest and lowest")
        void test_calculateAverageScore_removeHighLow() {
            List<BigDecimal> scores = Arrays.asList(
                new BigDecimal("9.50"), new BigDecimal("9.30"), new BigDecimal("9.40"),
                new BigDecimal("9.80"), new BigDecimal("8.90")
            );
            BigDecimal max = scores.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
            BigDecimal min = scores.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
            BigDecimal sum = scores.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal adjusted = sum.subtract(max).subtract(min);
            BigDecimal average = adjusted.divide(BigDecimal.valueOf(scores.size() - 2), 2, RoundingMode.HALF_UP);
            assertEquals(new BigDecimal("9.40"), average);
        }
    }

    @Nested
    @DisplayName("Score Validation Tests")
    class ScoreValidationTests {
        @ParameterizedTest
        @ValueSource(strings = {"0.00", "5.00", "9.99", "10.00"})
        @DisplayName("Valid score range (0-10)")
        void test_scoreRange_valid(String scoreStr) {
            BigDecimal score = new BigDecimal(scoreStr);
            assertTrue(score.compareTo(BigDecimal.ZERO) >= 0 && score.compareTo(BigDecimal.TEN) <= 0);
        }

        @ParameterizedTest
        @ValueSource(strings = {"-0.01", "-1.00", "10.01", "15.00"})
        @DisplayName("Invalid score range")
        void test_scoreRange_invalid(String scoreStr) {
            BigDecimal score = new BigDecimal(scoreStr);
            assertFalse(score.compareTo(BigDecimal.ZERO) >= 0 && score.compareTo(BigDecimal.TEN) <= 0);
        }

        @Test
        @DisplayName("Score precision should be 2 decimal places")
        void test_scorePrecision() {
            BigDecimal score = new BigDecimal("9.567");
            BigDecimal rounded = score.setScale(2, RoundingMode.HALF_UP);
            assertEquals(new BigDecimal("9.57"), rounded);
        }

        @Test
        @DisplayName("Original score should be preserved")
        void test_originalScore_preserved() {
            assertNotNull(validScore.getOriginalScore());
            assertEquals(validScore.getScore(), validScore.getOriginalScore());
        }
    }

    @Nested
    @DisplayName("Anomaly Detection Tests")
    class AnomalyDetectionTests {
        private static final BigDecimal DEVIATION_THRESHOLD = new BigDecimal("2.0");

        @Test
        @DisplayName("Detect large deviation from average")
        void test_detectAnomaly_largeDeviation() {
            BigDecimal currentScore = new BigDecimal("6.00");
            BigDecimal averageScore = new BigDecimal("9.00");
            BigDecimal deviation = currentScore.subtract(averageScore).abs();
            assertTrue(deviation.compareTo(DEVIATION_THRESHOLD) > 0);
        }

        @Test
        @DisplayName("Normal deviation should not be flagged")
        void test_detectAnomaly_normalDeviation() {
            BigDecimal currentScore = new BigDecimal("8.50");
            BigDecimal averageScore = new BigDecimal("9.00");
            BigDecimal deviation = currentScore.subtract(averageScore).abs();
            assertTrue(deviation.compareTo(DEVIATION_THRESHOLD) <= 0);
        }

        @ParameterizedTest
        @CsvSource({"9.00, 9.00, false", "8.50, 9.00, false", "7.00, 9.00, false", "6.50, 9.00, true", "5.00, 9.00, true"})
        @DisplayName("Anomaly detection with various deviations")
        void test_detectAnomaly_variousDeviations(String current, String average, boolean shouldFlag) {
            BigDecimal currentScore = new BigDecimal(current);
            BigDecimal averageScore = new BigDecimal(average);
            BigDecimal deviation = currentScore.subtract(averageScore).abs();
            boolean isAnomaly = deviation.compareTo(DEVIATION_THRESHOLD) > 0;
            assertEquals(shouldFlag, isAnomaly);
        }

        @Test
        @DisplayName("Detect all same scores (potential collusion)")
        void test_detectAnomaly_allSameScores() {
            List<BigDecimal> scores = Arrays.asList(
                new BigDecimal("9.00"), new BigDecimal("9.00"), new BigDecimal("9.00"),
                new BigDecimal("9.00"), new BigDecimal("9.00")
            );
            boolean allSame = scores.stream().distinct().count() == 1;
            assertTrue(allSame);
        }
    }

    @Nested
    @DisplayName("Score Modification Tests")
    class ModificationTests {
        @Test
        @DisplayName("Score modification should preserve original")
        void test_modifyScore_preservesOriginal() {
            BigDecimal original = new BigDecimal("9.50");
            validScore.setOriginalScore(original);
            validScore.setScore(new BigDecimal("9.20"));
            validScore.setModifyReason("Correction after review");
            validScore.setModifyTime(LocalDateTime.now());
            assertNotEquals(validScore.getScore(), validScore.getOriginalScore());
            assertNotNull(validScore.getModifyReason());
        }

        @Test
        @DisplayName("Modification requires reason")
        void test_modifyScore_requiresReason() {
            validScore.setModifyReason("Judge error correction");
            assertNotNull(validScore.getModifyReason());
            assertFalse(validScore.getModifyReason().isEmpty());
        }
    }
}
