package dev.awesomedev.mirim.service;

import dev.awesomedev.mirim.domain.CodingProblem;
import dev.awesomedev.mirim.domain.CodingSubmission;
import dev.awesomedev.mirim.domain.Progress;
import dev.awesomedev.mirim.domain.QuizResult;
import dev.awesomedev.mirim.domain.StudentProfile;
import dev.awesomedev.mirim.domain.User;
import dev.awesomedev.mirim.repository.ChecklistItemRepository;
import dev.awesomedev.mirim.repository.CodingProblemRepository;
import dev.awesomedev.mirim.repository.CodingSubmissionRepository;
import dev.awesomedev.mirim.repository.CourseProgressRepository;
import dev.awesomedev.mirim.repository.ProgressRepository;
import dev.awesomedev.mirim.repository.QuizResultRepository;
import dev.awesomedev.mirim.repository.StudentProfileRepository;
import dev.awesomedev.mirim.repository.SubmissionRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;

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

/**
 * 이 파일이 하는 일: 코호트 분석의 집계 로직을 검증한다 — 코호트 필터, 퀴즈 평균,
 * '마지막 활동/관심필요' 일수 계산, 어려운 지점(핫스팟) 정렬.
 *
 * 학습 포인트 ① — 시각(Clock)을 고정해 "며칠 전 활동"을 결정적으로 검증한다.
 * 학습 포인트 ② — Mockito 함정: when(...).thenReturn(X)의 X를 만드는 도중에 또 when(...)을
 *   부르면 "UnfinishedStubbing" 오류가 난다. 그래서 목 객체는 먼저 변수에 다 만들어 두고 넘긴다.
 */
class MentorAnalyticsServiceTest {

    private static final Instant NOW = Instant.parse("2026-09-10T09:00:00Z");
    private final Clock clock = Clock.fixed(NOW, ZoneOffset.UTC);

    private final StudentProfileRepository profileRepo = mock(StudentProfileRepository.class);
    private final QuizResultRepository quizRepo = mock(QuizResultRepository.class);
    private final CodingSubmissionRepository codeRepo = mock(CodingSubmissionRepository.class);
    private final CodingProblemRepository problemRepo = mock(CodingProblemRepository.class);
    private final CourseProgressRepository courseRepo = mock(CourseProgressRepository.class);
    private final ProgressRepository progressRepo = mock(ProgressRepository.class);
    private final ChecklistItemRepository checklistRepo = mock(ChecklistItemRepository.class);
    private final SubmissionRepository submissionRepo = mock(SubmissionRepository.class);

    private final MentorAnalyticsService service = new MentorAnalyticsService(
            profileRepo, quizRepo, codeRepo, problemRepo, courseRepo, progressRepo,
            checklistRepo, submissionRepo, clock);

    private User student(String name, String track) {
        return new User(name, "해시", name, "STUDENT", track);
    }

    private StudentProfile profile(User u, String cohort) {
        StudentProfile p = mock(StudentProfile.class);
        when(p.getUser()).thenReturn(u);
        when(p.getCohort()).thenReturn(cohort);
        return p;
    }

    @Test
    @DisplayName("코호트 목록과 인원수를 집계하고, 필터로 해당 기수만 남긴다")
    void cohorts_and_filter() {
        StudentProfile pa = profile(student("가", "DEV"), "2025");
        StudentProfile pb = profile(student("나", "DEV"), "2026");
        StudentProfile pc = profile(student("다", "DESIGN"), "2026");
        emptyActivity();
        when(profileRepo.findAllWithUser()).thenReturn(List.of(pa, pb, pc));

        var all = service.analytics(null);
        assertEquals(3, all.studentCount());
        assertEquals(List.of(
                new MentorAnalyticsService.CohortCount("2025", 1),
                new MentorAnalyticsService.CohortCount("2026", 2)), all.cohorts());

        var only2026 = service.analytics("2026");
        assertEquals(2, only2026.studentCount());
        assertTrue(only2026.students().stream().allMatch(s -> s.cohort().equals("2026")));
    }

    @Test
    @DisplayName("퀴즈는 코스별 최고점의 평균(%)을 내고, 마지막 활동 일수를 계산한다")
    void quiz_average_and_last_active() {
        User a = student("가", "DEV");
        StudentProfile pa = profile(a, "2026");
        // 같은 코스 2회 응시(최고점 채택: 5/5=100 vs 3/5=60 → 100), 다른 코스 1회(2/4=50)
        QuizResult q1 = quiz("c1", 5, 5, NOW.minus(Duration.ofDays(3)));
        QuizResult q2 = quiz("c1", 3, 5, NOW.minus(Duration.ofDays(10)));
        QuizResult q3 = quiz("c2", 2, 4, NOW.minus(Duration.ofDays(5)));
        emptyActivity();
        when(profileRepo.findAllWithUser()).thenReturn(List.of(pa));
        when(quizRepo.findByUserOrderBySubmittedAtDesc(a)).thenReturn(List.of(q1, q2, q3));

        var row = service.analytics(null).students().get(0);
        assertEquals(2, row.quizAttempted());
        assertEquals(75, row.quizAvgPct()); // (100 + 50) / 2
        assertEquals(3L, row.daysSinceActive()); // 가장 최근 활동이 3일 전
    }

    @Test
    @DisplayName("활동이 없으면 lastActive는 null, daysSinceActive도 null이다")
    void no_activity() {
        StudentProfile pa = profile(student("가", "DEV"), "2026");
        emptyActivity();
        when(profileRepo.findAllWithUser()).thenReturn(List.of(pa));

        var row = service.analytics(null).students().get(0);
        assertNull(row.lastActiveAt());
        assertNull(row.daysSinceActive());
        assertEquals(0, row.quizAvgPct());
    }

    @Test
    @DisplayName("체크리스트 체크만 한 학생도 그 시각이 '마지막 활동'에 반영된다")
    void checklist_counts_as_activity() {
        User a = student("가", "DEV");
        StudentProfile pa = profile(a, "2026");
        Progress pr = mock(Progress.class);
        when(pr.getCheckedAt()).thenReturn(NOW.minus(Duration.ofDays(2)));
        emptyActivity();
        when(profileRepo.findAllWithUser()).thenReturn(List.of(pa));
        when(progressRepo.findByUser(a)).thenReturn(List.of(pr));

        var row = service.analytics(null).students().get(0);
        assertEquals(2L, row.daysSinceActive()); // 체크리스트 체크(2일 전)가 활동으로 잡힘
        assertEquals(1, row.checklistDone());
    }

    @Test
    @DisplayName("어려운 코딩 문제는 통과율 낮은 순으로 정렬된다")
    void coding_hotspots_sorted_by_pass_rate() {
        User a = student("가", "DEV");
        User b = student("나", "DEV");
        StudentProfile pa = profile(a, "2026");
        StudentProfile pb = profile(b, "2026");
        CodingProblem easy = problem(10L, "쉬운문제");
        CodingProblem hard = problem(20L, "어려운문제");
        // easy: 둘 다 통과(100%). hard: 한 명만 통과(50%)
        CodingSubmission a1 = sub(easy, "PASSED");
        CodingSubmission a2 = sub(hard, "PASSED");
        CodingSubmission b1 = sub(easy, "PASSED");
        CodingSubmission b2 = sub(hard, "FAILED");
        emptyActivity();
        when(profileRepo.findAllWithUser()).thenReturn(List.of(pa, pb));
        when(codeRepo.findByUser(a)).thenReturn(List.of(a1, a2));
        when(codeRepo.findByUser(b)).thenReturn(List.of(b1, b2));

        var hot = service.analytics(null).hardestCoding();
        assertEquals(2, hot.size());
        assertEquals("어려운문제", hot.get(0).title()); // 통과율 낮은 게 먼저
        assertEquals(50, hot.get(0).passRatePct());
        assertEquals(100, hot.get(1).passRatePct());
    }

    // ── 헬퍼: 목 객체는 먼저 만들어 두고, when()에는 완성된 객체만 넘긴다 ──

    private void emptyActivity() {
        when(problemRepo.count()).thenReturn(25L);
        when(checklistRepo.count()).thenReturn(30L);
        when(quizRepo.findByUserOrderBySubmittedAtDesc(any())).thenReturn(List.of());
        when(codeRepo.findByUser(any())).thenReturn(List.of());
        when(courseRepo.findByUser(any())).thenReturn(List.of());
        when(progressRepo.findByUser(any())).thenReturn(List.of());
        when(submissionRepo.findByUserOrderBySubmittedAtDesc(any())).thenReturn(List.of());
    }

    private QuizResult quiz(String slug, int score, int total, Instant at) {
        QuizResult qr = mock(QuizResult.class);
        when(qr.getCourseSlug()).thenReturn(slug);
        when(qr.getScore()).thenReturn(score);
        when(qr.getTotal()).thenReturn(total);
        when(qr.getSubmittedAt()).thenReturn(at);
        return qr;
    }

    private CodingProblem problem(Long id, String title) {
        CodingProblem p = mock(CodingProblem.class);
        when(p.getId()).thenReturn(id);
        when(p.getTitle()).thenReturn(title);
        return p;
    }

    private CodingSubmission sub(CodingProblem problem, String status) {
        CodingSubmission cs = mock(CodingSubmission.class);
        when(cs.getProblem()).thenReturn(problem);
        when(cs.getStatus()).thenReturn(status);
        when(cs.getSubmittedAt()).thenReturn(NOW.minus(Duration.ofDays(2)));
        return cs;
    }
}
