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.InjectMocks;
import org.mockito.Mock;
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.MartialResult;
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.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ArrayList;

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

/**
 * MartialResultServiceImpl Unit Tests - Google Standard
 * Tests actual business logic: score calculation, ranking, medal assignment
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("MartialResultServiceImpl Business Logic Tests")
class MartialResultServiceImplTest {

    @InjectMocks
    private MartialResultServiceImpl resultService;

    @Mock
    private MartialResultMapper resultMapper;

    @Mock
    private IMartialScoreService scoreService;

    @Mock
    private IMartialAthleteService athleteService;

    @Mock
    private IMartialProjectService projectService;

    @Mock
    private IMartialCompetitionService competitionService;

    // ==================== calculateValidAverageScore() Logic Tests ====================

    @Nested
    @DisplayName("calculateValidAverageScore() - Remove Max/Min Logic")
    class CalculateValidAverageScoreTests {

        @Test
        @DisplayName("5 scores: remove max and min, average remaining 3")
        void calculateAverage_fiveScores_removesMaxMin() {
            // Scores: 9.0, 9.2, 9.5, 8.8, 9.1
            // Remove max (9.5) and min (8.8)
            // Average of 9.0, 9.2, 9.1 = 27.3 / 3 = 9.100
            List<BigDecimal> scores = Arrays.asList(
                new BigDecimal("9.0"),
                new BigDecimal("9.2"),
                new BigDecimal("9.5"), // max - removed
                new BigDecimal("8.8"), // min - removed
                new BigDecimal("9.1")
            );
            
            BigDecimal max = scores.stream().max(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
            BigDecimal min = scores.stream().min(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
            
            assertEquals(new BigDecimal("9.5"), max);
            assertEquals(new BigDecimal("8.8"), min);
            
            // Filter valid scores
            List<BigDecimal> validScores = new ArrayList<>();
            boolean maxRemoved = false;
            boolean minRemoved = false;
            for (BigDecimal score : scores) {
                if (!maxRemoved && score.compareTo(max) == 0) {
                    maxRemoved = true;
                    continue;
                }
                if (!minRemoved && score.compareTo(min) == 0) {
                    minRemoved = true;
                    continue;
                }
                validScores.add(score);
            }
            
            assertEquals(3, validScores.size());
            
            BigDecimal sum = validScores.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal avg = sum.divide(new BigDecimal(validScores.size()), 3, RoundingMode.HALF_UP);
            
            assertEquals(new BigDecimal("9.100"), avg);
        }

        @Test
        @DisplayName("3 scores minimum: remove max and min, average 1")
        void calculateAverage_threeScores_removesMaxMin() {
            // Scores: 9.0, 9.5, 8.5
            // Remove max (9.5) and min (8.5)
            // Average of 9.0 = 9.000
            List<BigDecimal> scores = Arrays.asList(
                new BigDecimal("9.0"),
                new BigDecimal("9.5"), // max
                new BigDecimal("8.5")  // min
            );
            
            BigDecimal max = scores.stream().max(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
            BigDecimal min = scores.stream().min(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
            
            List<BigDecimal> validScores = new ArrayList<>();
            boolean maxRemoved = false;
            boolean minRemoved = false;
            for (BigDecimal score : scores) {
                if (!maxRemoved && score.compareTo(max) == 0) {
                    maxRemoved = true;
                    continue;
                }
                if (!minRemoved && score.compareTo(min) == 0) {
                    minRemoved = true;
                    continue;
                }
                validScores.add(score);
            }
            
            assertEquals(1, validScores.size());
            assertEquals(new BigDecimal("9.0"), validScores.get(0));
        }

        @Test
        @DisplayName("duplicate max scores: only remove one")
        void calculateAverage_duplicateMax_removesOnlyOne() {
            // Scores: 9.5, 9.5, 9.0, 8.5
            // Remove one max (9.5) and min (8.5)
            // Remaining: 9.5, 9.0
            List<BigDecimal> scores = Arrays.asList(
                new BigDecimal("9.5"),
                new BigDecimal("9.5"),
                new BigDecimal("9.0"),
                new BigDecimal("8.5")
            );
            
            BigDecimal max = scores.stream().max(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
            BigDecimal min = scores.stream().min(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
            
            List<BigDecimal> validScores = new ArrayList<>();
            boolean maxRemoved = false;
            boolean minRemoved = false;
            for (BigDecimal score : scores) {
                if (!maxRemoved && score.compareTo(max) == 0) {
                    maxRemoved = true;
                    continue;
                }
                if (!minRemoved && score.compareTo(min) == 0) {
                    minRemoved = true;
                    continue;
                }
                validScores.add(score);
            }
            
            assertEquals(2, validScores.size());
            assertTrue(validScores.contains(new BigDecimal("9.5")));
            assertTrue(validScores.contains(new BigDecimal("9.0")));
        }

        @Test
        @DisplayName("less than 3 scores should fail")
        void calculateAverage_lessThan3Scores_shouldFail() {
            List<BigDecimal> scores = Arrays.asList(
                new BigDecimal("9.0"),
                new BigDecimal("9.5")
            );
            
            assertTrue(scores.size() < 3, "Should have less than 3 scores");
            // Business rule: cannot remove max/min with less than 3 judges
        }

        @Test
        @DisplayName("empty scores should fail")
        void calculateAverage_emptyScores_shouldFail() {
            List<BigDecimal> scores = Collections.emptyList();
            assertTrue(scores.isEmpty(), "Should have no scores");
        }
    }

    // ==================== applyDifficultyCoefficient() Tests ====================

    @Nested
    @DisplayName("applyDifficultyCoefficient() - Difficulty Multiplier")
    class ApplyDifficultyCoefficientTests {

        @Test
        @DisplayName("coefficient 1.0 returns same score")
        void applyCoefficient_1_0_returnsSameScore() {
            BigDecimal averageScore = new BigDecimal("9.000");
            BigDecimal coefficient = new BigDecimal("1.00");
            
            BigDecimal result = averageScore.multiply(coefficient)
                .setScale(3, RoundingMode.HALF_UP);
            
            assertEquals(new BigDecimal("9.000"), result);
        }

        @Test
        @DisplayName("coefficient 1.2 increases score by 20%")
        void applyCoefficient_1_2_increasesScore() {
            BigDecimal averageScore = new BigDecimal("9.000");
            BigDecimal coefficient = new BigDecimal("1.20");
            
            BigDecimal result = averageScore.multiply(coefficient)
                .setScale(3, RoundingMode.HALF_UP);
            
            assertEquals(new BigDecimal("10.800"), result);
        }

        @Test
        @DisplayName("coefficient 0.8 decreases score by 20%")
        void applyCoefficient_0_8_decreasesScore() {
            BigDecimal averageScore = new BigDecimal("9.000");
            BigDecimal coefficient = new BigDecimal("0.80");
            
            BigDecimal result = averageScore.multiply(coefficient)
                .setScale(3, RoundingMode.HALF_UP);
            
            assertEquals(new BigDecimal("7.200"), result);
        }

        @ParameterizedTest
        @CsvSource({
            "9.000, 1.00, 9.000",
            "9.000, 1.10, 9.900",
            "9.000, 1.20, 10.800",
            "8.500, 1.15, 9.775",
            "10.000, 0.90, 9.000"
        })
        @DisplayName("various coefficient calculations")
        void applyCoefficient_variousValues(String avg, String coef, String expected) {
            BigDecimal averageScore = new BigDecimal(avg);
            BigDecimal coefficient = new BigDecimal(coef);
            
            BigDecimal result = averageScore.multiply(coefficient)
                .setScale(3, RoundingMode.HALF_UP);
            
            assertEquals(new BigDecimal(expected), result);
        }

        @Test
        @DisplayName("null coefficient defaults to 1.00")
        void applyCoefficient_null_defaultsTo1() {
            BigDecimal coefficient = null;
            BigDecimal defaultCoefficient = (coefficient == null) ? new BigDecimal("1.00") : coefficient;
            
            assertEquals(new BigDecimal("1.00"), defaultCoefficient);
        }
    }

    // ==================== Ranking Logic Tests ====================

    @Nested
    @DisplayName("autoRanking() - Ranking Logic")
    class RankingLogicTests {

        @Test
        @DisplayName("rank by final score descending")
        void ranking_byFinalScoreDescending() {
            List<MartialResult> results = Arrays.asList(
                createResult(1L, new BigDecimal("9.500")),
                createResult(2L, new BigDecimal("9.800")),
                createResult(3L, new BigDecimal("9.200"))
            );
            
            // Sort by final score descending
            results.sort((a, b) -> b.getFinalScore().compareTo(a.getFinalScore()));
            
            assertEquals(2L, results.get(0).getAthleteId()); // 9.800 - 1st
            assertEquals(1L, results.get(1).getAthleteId()); // 9.500 - 2nd
            assertEquals(3L, results.get(2).getAthleteId()); // 9.200 - 3rd
        }

        @Test
        @DisplayName("tied scores get same rank")
        void ranking_tiedScores_sameRank() {
            List<MartialResult> results = Arrays.asList(
                createResult(1L, new BigDecimal("9.500")),
                createResult(2L, new BigDecimal("9.500")), // tied
                createResult(3L, new BigDecimal("9.200"))
            );
            
            results.sort((a, b) -> b.getFinalScore().compareTo(a.getFinalScore()));
            
            // Assign ranks
            int rank = 1;
            for (int i = 0; i < results.size(); i++) {
                if (i > 0 && results.get(i).getFinalScore().compareTo(results.get(i-1).getFinalScore()) != 0) {
                    rank = i + 1;
                }
                results.get(i).setRanking(rank);
            }
            
            assertEquals(1, results.get(0).getRanking()); // 9.500 - rank 1
            assertEquals(1, results.get(1).getRanking()); // 9.500 - rank 1 (tied)
            assertEquals(3, results.get(2).getRanking()); // 9.200 - rank 3 (skips 2)
        }
    }

    // ==================== Medal Assignment Tests ====================

    @Nested
    @DisplayName("assignMedals() - Medal Logic")
    class MedalAssignmentTests {

        @Test
        @DisplayName("rank 1 gets gold medal")
        void medal_rank1_gold() {
            int rank = 1;
            String medal = getMedalByRank(rank);
            assertEquals("gold", medal);
        }

        @Test
        @DisplayName("rank 2 gets silver medal")
        void medal_rank2_silver() {
            int rank = 2;
            String medal = getMedalByRank(rank);
            assertEquals("silver", medal);
        }

        @Test
        @DisplayName("rank 3 gets bronze medal")
        void medal_rank3_bronze() {
            int rank = 3;
            String medal = getMedalByRank(rank);
            assertEquals("bronze", medal);
        }

        @Test
        @DisplayName("rank > 3 gets no medal")
        void medal_rank4Plus_none() {
            assertEquals(null, getMedalByRank(4));
            assertEquals(null, getMedalByRank(5));
            assertEquals(null, getMedalByRank(100));
        }

        @Test
        @DisplayName("tied rank 1 both get gold")
        void medal_tiedRank1_bothGold() {
            // Two athletes tied at rank 1
            assertEquals("gold", getMedalByRank(1));
            assertEquals("gold", getMedalByRank(1));
        }

        private String getMedalByRank(int rank) {
            switch (rank) {
                case 1: return "gold";
                case 2: return "silver";
                case 3: return "bronze";
                default: return null;
            }
        }
    }

    // ==================== Result Status Tests ====================

    @Nested
    @DisplayName("Result Status Transitions")
    class ResultStatusTests {

        @Test
        @DisplayName("status 0 = pending calculation")
        void status_0_pendingCalculation() {
            MartialResult result = new MartialResult();
            result.setStatus(0);
            assertEquals(0, result.getStatus());
        }

        @Test
        @DisplayName("status 1 = calculated")
        void status_1_calculated() {
            MartialResult result = new MartialResult();
            result.setStatus(1);
            assertEquals(1, result.getStatus());
        }

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

        @Test
        @DisplayName("cannot unpublish non-published result")
        void unpublish_nonPublished_shouldFail() {
            MartialResult result = new MartialResult();
            result.setStatus(1); // calculated, not published
            
            // Business rule: can only unpublish if status == 2
            assertFalse(result.getStatus() == 2, "Should not be able to unpublish");
        }
    }

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

    private MartialResult createResult(Long athleteId, BigDecimal finalScore) {
        MartialResult result = new MartialResult();
        result.setId(athleteId);
        result.setAthleteId(athleteId);
        result.setProjectId(1L);
        result.setCompetitionId(1L);
        result.setFinalScore(finalScore);
        result.setStatus(1);
        return result;
    }
}
