import { describe, it, expect } from "vitest"

import {
  computeGradeStats,
  computeAverageScore,
  computeGradeDistribution,
  computeClassComparisonStats,
  computeSubjectComparisonStats,
  buildGradeTrendPoints,
  buildGrowthArchivePoints,
  buildRankingTrendPoints,
  detectScoreAnomaly,
  computeSignificance,
  findBucketIndex,
  PASS_THRESHOLD,
  EXCELLENT_THRESHOLD,
  type RawScoreRow,
  type RankingTrendEntry,
  type GrowthArchiveRawRow,
} from "./stats-service"
import type { GradeTrendPoint } from "./types"

describe("stats-service 常量", () => {
  it("PASS_THRESHOLD 应为 60", () => {
    expect(PASS_THRESHOLD).toBe(60)
  })
  it("EXCELLENT_THRESHOLD 应为 85", () => {
    expect(EXCELLENT_THRESHOLD).toBe(85)
  })
})

describe("computeGradeStats", () => {
  it("空数组应返回 null", () => {
    expect(computeGradeStats([])).toBeNull()
  })

  it("正确计算均分、中位数、最高分、最低分", () => {
    const rows: RawScoreRow[] = [
      { score: 80, fullScore: 100 },
      { score: 90, fullScore: 100 },
      { score: 70, fullScore: 100 },
      { score: 100, fullScore: 100 },
    ]
    const stats = computeGradeStats(rows)
    expect(stats).not.toBeNull()
    expect(stats!.average).toBe(85)
    expect(stats!.median).toBe(85)
    expect(stats!.max).toBe(100)
    expect(stats!.min).toBe(70)
    expect(stats!.count).toBe(4)
  })

  it("正确计算及格率和优秀率（百分比 0-100）", () => {
    const rows: RawScoreRow[] = [
      { score: 60, fullScore: 100 },
      { score: 90, fullScore: 100 },
      { score: 50, fullScore: 100 },
      { score: 86, fullScore: 100 },
    ]
    const stats = computeGradeStats(rows)
    expect(stats).not.toBeNull()
    // 及格（≥60%）：3/4 = 75%
    expect(stats!.passRate).toBe(75)
    // 优秀（≥85%）：2/4 = 50%
    expect(stats!.excellentRate).toBe(50)
  })

  it("处理字符串类型的分数（DB 驱动可能返回字符串）", () => {
    const rows: RawScoreRow[] = [
      { score: "80", fullScore: "100" },
      { score: "90", fullScore: "100" },
    ]
    const stats = computeGradeStats(rows)
    expect(stats).not.toBeNull()
    expect(stats!.average).toBe(85)
    expect(stats!.count).toBe(2)
  })

  it("单条记录应正确返回", () => {
    const rows: RawScoreRow[] = [{ score: 75, fullScore: 100 }]
    const stats = computeGradeStats(rows)
    expect(stats).not.toBeNull()
    expect(stats!.average).toBe(75)
    expect(stats!.median).toBe(75)
    expect(stats!.max).toBe(75)
    expect(stats!.min).toBe(75)
    expect(stats!.stdDev).toBe(0)
  })
})

describe("computeAverageScore", () => {
  it("空数组返回 0", () => {
    expect(computeAverageScore([])).toBe(0)
  })
  it("正确计算平均值", () => {
    expect(computeAverageScore([80, 90, 70])).toBe(80)
  })
})

describe("computeGradeDistribution", () => {
  it("正确分布到 5 个区间", () => {
    const rows: RawScoreRow[] = [
      { score: 95, fullScore: 100 }, // 90-100
      { score: 85, fullScore: 100 }, // 80-89
      { score: 75, fullScore: 100 }, // 70-79
      { score: 65, fullScore: 100 }, // 60-69
      { score: 55, fullScore: 100 }, // <60
      { score: 100, fullScore: 100 }, // 90-100
    ]
    const result = computeGradeDistribution(rows)
    expect(result.buckets).toHaveLength(5)
    expect(result.buckets[0].count).toBe(2) // 90-100
    expect(result.buckets[1].count).toBe(1) // 80-89
    expect(result.buckets[2].count).toBe(1) // 70-79
    expect(result.buckets[3].count).toBe(1) // 60-69
    expect(result.buckets[4].count).toBe(1) // <60
    expect(result.totalCount).toBe(6)
  })
})

describe("computeClassComparisonStats", () => {
  it("正确计算班级对比统计", () => {
    const rows = [
      { score: 80, fullScore: 100, studentId: "s1" },
      { score: 90, fullScore: 100, studentId: "s2" },
      { score: 70, fullScore: 100, studentId: "s3" },
    ]
    const stats = computeClassComparisonStats(rows)
    expect(stats.averageScore).toBe(80)
    expect(stats.studentCount).toBe(3)
    expect(stats.passRate).toBe(100) // 全部及格（百分比）
    expect(stats.excellentRate).toBeCloseTo(33.33, 1) // 仅 90 分优秀
  })
})

describe("computeSubjectComparisonStats", () => {
  it("正确计算科目对比统计", () => {
    const scores = [80, 90, 70, 60]
    const stats = computeSubjectComparisonStats(scores)
    expect(stats.averageScore).toBe(75)
    expect(stats.passRate).toBe(100) // 全部及格（百分比，≥60）
    expect(stats.count).toBe(4)
  })
})

describe("buildGradeTrendPoints", () => {
  it("按考试记录生成趋势点并归一化", () => {
    const rows = [
      {
        record: {
          createdAt: new Date("2024-01-01"),
          title: "期中考试",
          score: 80,
          fullScore: 100,
          type: "exam",
        },
      },
      {
        record: {
          createdAt: new Date("2024-06-01"),
          title: "期末考试",
          score: 40,
          fullScore: 50,
          type: "exam",
        },
      },
    ]
    const points = buildGradeTrendPoints(rows)
    expect(points).toHaveLength(2)
    // 归一化后均为 80
    expect(points[0].normalizedScore).toBe(80)
    expect(points[1].normalizedScore).toBe(80)
    expect(points[0].title).toBe("期中考试")
  })

  it("未知类型安全降级为 other", () => {
    const rows = [
      {
        record: {
          createdAt: new Date("2024-01-01"),
          title: "测试",
          score: 80,
          fullScore: 100,
          type: "unknown_type",
        },
      },
    ]
    const points = buildGradeTrendPoints(rows)
    expect(points[0].type).toBe("other")
  })
})

describe("buildRankingTrendPoints", () => {
  it("按日期升序排列", () => {
    const byTitle = new Map<string, RankingTrendEntry>([
      [
        "期中考试",
        {
          date: new Date("2024-01-01"),
          entries: [
            { studentId: "s1", normalized: 90 },
            { studentId: "s2", normalized: 80 },
          ],
        },
      ],
      [
        "期末考试",
        {
          date: new Date("2024-06-01"),
          entries: [
            { studentId: "s1", normalized: 85 },
            { studentId: "s2", normalized: 95 },
          ],
        },
      ],
    ])
    const points = buildRankingTrendPoints(byTitle, "s1")
    expect(points).toHaveLength(2)
    // 期中 s1 排第 1
    expect(points[0].rank).toBe(1)
    expect(points[0].score).toBe(90)
    // 期末 s1 排第 2
    expect(points[1].rank).toBe(2)
    expect(points[1].score).toBe(85)
    // 按日期升序
    expect(new Date(points[0].date).getTime()).toBeLessThan(new Date(points[1].date).getTime())
  })

  it("目标学生不存在时返回空数组", () => {
    const byTitle = new Map<string, RankingTrendEntry>([
      [
        "考试",
        {
          date: new Date("2024-01-01"),
          entries: [{ studentId: "s1", normalized: 90 }],
        },
      ],
    ])
    const points = buildRankingTrendPoints(byTitle, "unknown")
    expect(points).toHaveLength(0)
  })
})

describe("detectScoreAnomaly (P3-2)", () => {
  it("数据点不足时返回 isAnomaly=false", () => {
    const points: Pick<GradeTrendPoint, "normalizedScore">[] = [
      { normalizedScore: 80 },
      { normalizedScore: 75 },
    ]
    const result = detectScoreAnomaly(points)
    expect(result.isAnomaly).toBe(false)
    expect(result.severity).toBeNull()
  })

  it("成绩上升不触发异常", () => {
    const points: Pick<GradeTrendPoint, "normalizedScore">[] = [
      { normalizedScore: 70 },
      { normalizedScore: 75 },
      { normalizedScore: 80 },
    ]
    const result = detectScoreAnomaly(points)
    expect(result.isAnomaly).toBe(false)
    expect(result.drop).toBeLessThanOrEqual(0)
  })

  it("下降超过 10 分触发 warning", () => {
    const points: Pick<GradeTrendPoint, "normalizedScore">[] = [
      { normalizedScore: 85 },
      { normalizedScore: 83 },
      { normalizedScore: 70 },
    ]
    const result = detectScoreAnomaly(points)
    expect(result.isAnomaly).toBe(true)
    expect(result.severity).toBe("warning")
    expect(result.drop).toBeGreaterThanOrEqual(10)
    expect(result.latestScore).toBe(70)
    expect(result.historicalAverage).toBe(84)
  })

  it("下降超过 20 分触发 critical", () => {
    const points: Pick<GradeTrendPoint, "normalizedScore">[] = [
      { normalizedScore: 90 },
      { normalizedScore: 88 },
      { normalizedScore: 60 },
    ]
    const result = detectScoreAnomaly(points)
    expect(result.isAnomaly).toBe(true)
    expect(result.severity).toBe("critical")
    expect(result.drop).toBeGreaterThanOrEqual(20)
  })

  it("相对下降超过 20% 触发 critical（即使绝对值不足 20）", () => {
    const points: Pick<GradeTrendPoint, "normalizedScore">[] = [
      { normalizedScore: 50 },
      { normalizedScore: 48 },
      { normalizedScore: 38 },
    ]
    const result = detectScoreAnomaly(points)
    expect(result.isAnomaly).toBe(true)
    // 历史平均 49，最新 38，绝对下降 11（warning），相对下降 22%（critical）
    expect(result.severity).toBe("critical")
  })

  it("历史平均为 0 时不触发异常", () => {
    const points: Pick<GradeTrendPoint, "normalizedScore">[] = [
      { normalizedScore: 0 },
      { normalizedScore: 0 },
      { normalizedScore: 50 },
    ]
    const result = detectScoreAnomaly(points)
    expect(result.isAnomaly).toBe(false)
  })
})

describe("computeSignificance (P3-5)", () => {
  it("样本不足时返回 null", () => {
    expect(computeSignificance([80], [90])).toBeNull()
    expect(computeSignificance([], [90, 85])).toBeNull()
  })

  it("两组相同数据返回 negligible + 不显著", () => {
    const result = computeSignificance([80, 80, 80], [80, 80, 80])
    expect(result).not.toBeNull()
    expect(result!.effectSizeLabel).toBe("negligible")
    expect(result!.isSignificant).toBe(false)
    expect(result!.cohensD).toBe(0)
  })

  it("显著差异的大样本返回 isSignificant=true", () => {
    // 两个差异很大的大样本
    const classA = Array.from({ length: 30 }, (_, i) => 90 - i * 0.5) // 90→75.5
    const classB = Array.from({ length: 30 }, (_, i) => 60 + i * 0.5) // 60→74.5
    const result = computeSignificance(classA, classB)
    expect(result).not.toBeNull()
    expect(result!.cohensD).toBeGreaterThan(0.8) // large effect
    expect(result!.effectSizeLabel).toBe("large")
    expect(result!.isSignificant).toBe(true)
  })

  it("微小差异返回 negligible", () => {
    const result = computeSignificance([80, 81, 79, 80], [80, 79, 81, 80])
    expect(result).not.toBeNull()
    expect(result!.effectSizeLabel).toBe("negligible")
  })

  it("p-value 范围在 [0, 1]", () => {
    const result = computeSignificance([80, 85, 90], [75, 70, 65])
    expect(result).not.toBeNull()
    expect(result!.pValue).toBeGreaterThanOrEqual(0)
    expect(result!.pValue).toBeLessThanOrEqual(1)
  })
})

describe("findBucketIndex (P3-9)", () => {
  const buckets = [
    { label: "90-100", min: 90, max: 100, count: 0 },
    { label: "80-89", min: 80, max: 89, count: 0 },
    { label: "70-79", min: 70, max: 79, count: 0 },
    { label: "60-69", min: 60, max: 69, count: 0 },
    { label: "<60", min: 0, max: 59, count: 0 },
  ]

  it("分数 95 应在 90-100 桶", () => {
    expect(findBucketIndex(buckets, 95)).toBe(0)
  })

  it("分数 85 应在 80-89 桶", () => {
    expect(findBucketIndex(buckets, 85)).toBe(1)
  })

  it("分数 75 应在 70-79 桶", () => {
    expect(findBucketIndex(buckets, 75)).toBe(2)
  })

  it("分数 65 应在 60-69 桶", () => {
    expect(findBucketIndex(buckets, 65)).toBe(3)
  })

  it("分数 50 应在 <60 桶", () => {
    expect(findBucketIndex(buckets, 50)).toBe(4)
  })

  it("边界分数 90 应在 90-100 桶", () => {
    expect(findBucketIndex(buckets, 90)).toBe(0)
  })

  it("边界分数 80 应在 80-89 桶（不进位到 90-100）", () => {
    expect(findBucketIndex(buckets, 80)).toBe(1)
  })

  it("分数四舍五入后归入对应桶", () => {
    expect(findBucketIndex(buckets, 89.6)).toBe(0)
  })

  it("分数 0 应在 <60 桶", () => {
    expect(findBucketIndex(buckets, 0)).toBe(4)
  })

  it("超出范围的负数应返回 -1", () => {
    expect(findBucketIndex(buckets, -10)).toBe(-1)
  })

  it("超出 100 的分数应返回 -1", () => {
    expect(findBucketIndex(buckets, 110)).toBe(-1)
  })

  it("空桶列表应返回 -1", () => {
    expect(findBucketIndex([], 50)).toBe(-1)
  })
})

describe("buildGrowthArchivePoints (P3-4)", () => {
  const baseRow = {
    academicYearId: "year-2024",
    academicYearName: "2024-2025",
    academicYearStart: new Date("2024-09-01"),
    subjectId: "subject-math",
  }

  it("空数组应返回空数组", () => {
    expect(buildGrowthArchivePoints([])).toEqual([])
  })

  it("academicYearId 为 null 的行应被跳过", () => {
    const rows: GrowthArchiveRawRow[] = [
      {
        ...baseRow,
        academicYearId: null,
        semester: "1",
        score: 80,
        fullScore: 100,
        title: "Midterm",
      },
    ]
    expect(buildGrowthArchivePoints(rows)).toEqual([])
  })

  it("单学年单学期应返回单个数据点", () => {
    const rows: GrowthArchiveRawRow[] = [
      {
        ...baseRow,
        semester: "1",
        score: 80,
        fullScore: 100,
        title: "Midterm",
      },
      {
        ...baseRow,
        semester: "1",
        score: 90,
        fullScore: 100,
        title: "Final",
      },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(points).toHaveLength(1)
    expect(points[0].academicYearId).toBe("year-2024")
    expect(points[0].semester).toBe("1")
    expect(points[0].averageScore).toBe(85)
    expect(points[0].recordCount).toBe(2)
    expect(points[0].subjectCount).toBe(1)
    expect(points[0].assessmentCount).toBe(2)
  })

  it("多学年多学期应按 (year start, semester) 升序排列", () => {
    const rows: GrowthArchiveRawRow[] = [
      // 2025 学年 第一学期（应排在最后）
      {
        ...baseRow,
        academicYearId: "year-2025",
        academicYearName: "2025-2026",
        academicYearStart: new Date("2025-09-01"),
        semester: "1",
        score: 80,
        fullScore: 100,
        title: "Midterm",
      },
      // 2024 学年 第二学期（应排第二）
      {
        ...baseRow,
        semester: "2",
        score: 85,
        fullScore: 100,
        title: "Final",
      },
      // 2024 学年 第一学期（应排第一）
      {
        ...baseRow,
        semester: "1",
        score: 75,
        fullScore: 100,
        title: "Midterm",
      },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(points).toHaveLength(3)
    expect(points[0].academicYearId).toBe("year-2024")
    expect(points[0].semester).toBe("1")
    expect(points[1].academicYearId).toBe("year-2024")
    expect(points[1].semester).toBe("2")
    expect(points[2].academicYearId).toBe("year-2025")
    expect(points[2].semester).toBe("1")
  })

  it("应正确计算及格率和优秀率（百分比 0-100）", () => {
    const rows: GrowthArchiveRawRow[] = [
      // 60 → 及格
      { ...baseRow, semester: "1", score: 60, fullScore: 100, title: "T1" },
      // 90 → 及格 + 优秀
      { ...baseRow, semester: "1", score: 90, fullScore: 100, title: "T2" },
      // 50 → 不及格
      { ...baseRow, semester: "1", score: 50, fullScore: 100, title: "T3" },
      // 86 → 及格 + 优秀
      { ...baseRow, semester: "1", score: 86, fullScore: 100, title: "T4" },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(points).toHaveLength(1)
    // 3/4 = 75% 及格
    expect(points[0].passRate).toBe(75)
    // 2/4 = 50% 优秀
    expect(points[0].excellentRate).toBe(50)
  })

  it("应正确统计科目数和考试数", () => {
    const rows: GrowthArchiveRawRow[] = [
      {
        ...baseRow,
        subjectId: "math",
        semester: "1",
        score: 80,
        fullScore: 100,
        title: "Midterm",
      },
      {
        ...baseRow,
        subjectId: "english",
        semester: "1",
        score: 85,
        fullScore: 100,
        title: "Midterm",
      },
      {
        ...baseRow,
        subjectId: "math",
        semester: "1",
        score: 90,
        fullScore: 100,
        title: "Final",
      },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(points).toHaveLength(1)
    // 2 个科目（math、english）
    expect(points[0].subjectCount).toBe(2)
    // 2 个不同的考试（Midterm、Final）
    expect(points[0].assessmentCount).toBe(2)
  })

  it("字符串类型分数应正确转换", () => {
    const rows: GrowthArchiveRawRow[] = [
      {
        ...baseRow,
        semester: "1",
        score: "80",
        fullScore: "100",
        title: "Midterm",
      },
      {
        ...baseRow,
        semester: "1",
        score: "90",
        fullScore: "100",
        title: "Final",
      },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(points).toHaveLength(1)
    expect(points[0].averageScore).toBe(85)
    expect(points[0].recordCount).toBe(2)
  })

  it("归一化分数应正确处理非 100 分制", () => {
    const rows: GrowthArchiveRawRow[] = [
      // 80/100 = 80
      {
        ...baseRow,
        semester: "1",
        score: 80,
        fullScore: 100,
        title: "T1",
      },
      // 40/50 = 80
      {
        ...baseRow,
        semester: "1",
        score: 40,
        fullScore: 50,
        title: "T2",
      },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(points[0].averageScore).toBe(80)
  })

  it("不同学年同学期应分组成不同点", () => {
    const rows: GrowthArchiveRawRow[] = [
      {
        ...baseRow,
        academicYearId: "year-2024",
        academicYearName: "2024-2025",
        academicYearStart: new Date("2024-09-01"),
        semester: "1",
        score: 80,
        fullScore: 100,
        title: "T1",
      },
      {
        ...baseRow,
        academicYearId: "year-2025",
        academicYearName: "2025-2026",
        academicYearStart: new Date("2025-09-01"),
        semester: "1",
        score: 90,
        fullScore: 100,
        title: "T1",
      },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(points).toHaveLength(2)
    expect(points[0].academicYearId).toBe("year-2024")
    expect(points[0].averageScore).toBe(80)
    expect(points[1].academicYearId).toBe("year-2025")
    expect(points[1].averageScore).toBe(90)
  })

  it("中位数应正确计算（偶数个数据点）", () => {
    const rows: GrowthArchiveRawRow[] = [
      { ...baseRow, semester: "1", score: 70, fullScore: 100, title: "T1" },
      { ...baseRow, semester: "1", score: 80, fullScore: 100, title: "T2" },
      { ...baseRow, semester: "1", score: 90, fullScore: 100, title: "T3" },
      { ...baseRow, semester: "1", score: 100, fullScore: 100, title: "T4" },
    ]
    const points = buildGrowthArchivePoints(rows)
    // 排序后 [70, 80, 90, 100]，中位数 = (80 + 90) / 2 = 85
    expect(points[0].medianScore).toBe(85)
  })

  it("academicYearStart 应转换为 ISO 字符串", () => {
    const rows: GrowthArchiveRawRow[] = [
      {
        ...baseRow,
        semester: "1",
        score: 80,
        fullScore: 100,
        title: "T1",
      },
    ]
    const points = buildGrowthArchivePoints(rows)
    expect(typeof points[0].academicYearStart).toBe("string")
    expect(new Date(points[0].academicYearStart).getTime()).toBe(
      new Date("2024-09-01").getTime()
    )
  })
})
