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

import {
  parseSchedule,
  isScheduleConflict,
  normalizeDay,
  buildLotteryRankCase,
} from "@/modules/elective/data-access-operations"
import { mapCourseRow, type CourseCoreRow } from "@/modules/elective/data-access"

/**
 * Elective 模块纯函数单元测试（P2-18 新增）。
 *
 * 覆盖范围：
 * - normalizeDay：中英文星期归一化
 * - parseSchedule：时间段解析（单时段、多时段、中英文、非法输入）
 * - isScheduleConflict：冲突检测（同天重叠、跨天不冲突、归一化后冲突）
 * - buildLotteryRankCase：抽签 rank SQL 构建结构
 * - mapCourseRow：数据库行 → 业务对象映射
 *
 * 这些纯函数不依赖 DB，可在 vitest node 环境下独立运行。
 */

describe("normalizeDay", () => {
  it("maps Chinese 周一-周天 to 1-7", () => {
    expect(normalizeDay("周一")).toBe("1")
    expect(normalizeDay("周二")).toBe("2")
    expect(normalizeDay("周三")).toBe("3")
    expect(normalizeDay("周四")).toBe("4")
    expect(normalizeDay("周五")).toBe("5")
    expect(normalizeDay("周六")).toBe("6")
    expect(normalizeDay("周日")).toBe("7")
    expect(normalizeDay("周天")).toBe("7")
  })

  it("maps Chinese 星期一-星期日 to 1-7", () => {
    expect(normalizeDay("星期一")).toBe("1")
    expect(normalizeDay("星期二")).toBe("2")
    expect(normalizeDay("星期日")).toBe("7")
    expect(normalizeDay("星期天")).toBe("7")
  })

  it("maps English full day names case-insensitively", () => {
    expect(normalizeDay("monday")).toBe("1")
    expect(normalizeDay("Tuesday")).toBe("2")
    expect(normalizeDay("WEDNESDAY")).toBe("3")
    expect(normalizeDay("Thursday")).toBe("4")
    expect(normalizeDay("friday")).toBe("5")
    expect(normalizeDay("Saturday")).toBe("6")
    expect(normalizeDay("sunday")).toBe("7")
  })

  it("maps English abbreviations case-insensitively", () => {
    expect(normalizeDay("mon")).toBe("1")
    expect(normalizeDay("Tue")).toBe("2")
    expect(normalizeDay("WED")).toBe("3")
    expect(normalizeDay("thu")).toBe("4")
    expect(normalizeDay("fri")).toBe("5")
    expect(normalizeDay("sat")).toBe("6")
    expect(normalizeDay("sun")).toBe("7")
  })

  it("returns input unchanged for unknown values", () => {
    expect(normalizeDay("foo")).toBe("foo")
    expect(normalizeDay("")).toBe("")
  })
})

describe("parseSchedule", () => {
  it("returns empty array for null/empty input", () => {
    expect(parseSchedule(null)).toEqual([])
    expect(parseSchedule("")).toEqual([])
    expect(parseSchedule("   ")).toEqual([])
  })

  it("parses single Chinese schedule", () => {
    const result = parseSchedule("周一 14:00-15:30")
    expect(result).toHaveLength(1)
    expect(result[0]).toEqual({
      day: "周一",
      start: "14:00",
      end: "15:30",
    })
  })

  it("parses single English full day name", () => {
    const result = parseSchedule("Monday 09:00-10:30")
    expect(result).toHaveLength(1)
    expect(result[0]).toEqual({
      day: "Monday",
      start: "09:00",
      end: "10:30",
    })
  })

  it("parses single English abbreviation", () => {
    const result = parseSchedule("Mon 14:00-15:30")
    expect(result).toHaveLength(1)
    expect(result[0]).toEqual({
      day: "Mon",
      start: "14:00",
      end: "15:30",
    })
  })

  it("parses 星期一 format", () => {
    const result = parseSchedule("星期一 08:00-09:00")
    expect(result).toHaveLength(1)
    expect(result[0]).toEqual({
      day: "星期一",
      start: "08:00",
      end: "09:00",
    })
  })

  it("parses multiple time slots separated by comma", () => {
    const result = parseSchedule("周一 14:00-15:30, Wed 16:00-17:30")
    expect(result).toHaveLength(2)
    expect(result[0]).toEqual({ day: "周一", start: "14:00", end: "15:30" })
    expect(result[1]).toEqual({ day: "Wed", start: "16:00", end: "17:30" })
  })

  it("parses multiple time slots separated by Chinese semicolon", () => {
    const result = parseSchedule("周一 14:00-15:30；周三 16:00-17:30")
    expect(result).toHaveLength(2)
    expect(result[0]).toEqual({ day: "周一", start: "14:00", end: "15:30" })
    expect(result[1]).toEqual({ day: "周三", start: "16:00", end: "17:30" })
  })

  it("parses multiple time slots separated by English semicolon", () => {
    const result = parseSchedule("Mon 14:00-15:30; Wed 16:00-17:30")
    expect(result).toHaveLength(2)
  })

  it("supports ~ and ～ and 至 as time range separators", () => {
    expect(parseSchedule("周一 14:00~15:30")).toHaveLength(1)
    expect(parseSchedule("周一 14:00～15:30")).toHaveLength(1)
    expect(parseSchedule("周一 14:00至15:30")).toHaveLength(1)
  })

  it("pads single-digit hour with leading zero", () => {
    const result = parseSchedule("周一 9:00-10:30")
    expect(result).toHaveLength(1)
    expect(result[0]?.start).toBe("09:00")
    expect(result[0]?.end).toBe("10:30")
  })

  it("returns empty array for invalid format", () => {
    expect(parseSchedule("invalid")).toEqual([])
    expect(parseSchedule("周一")).toEqual([])
    expect(parseSchedule("周一 14:00")).toEqual([])
    expect(parseSchedule("周一 invalid-invalid")).toEqual([])
  })

  it("skips invalid segments in multi-slot input but keeps valid ones", () => {
    const result = parseSchedule("周一 14:00-15:30, invalid, Wed 16:00-17:30")
    expect(result).toHaveLength(2)
  })
})

describe("isScheduleConflict", () => {
  it("returns false for different days", () => {
    const a = { day: "周一", start: "14:00", end: "15:30" }
    const b = { day: "周二", start: "14:00", end: "15:30" }
    expect(isScheduleConflict(a, b)).toBe(false)
  })

  it("returns true for overlapping time on same day (Chinese)", () => {
    const a = { day: "周一", start: "14:00", end: "15:30" }
    const b = { day: "周一", start: "15:00", end: "16:00" }
    expect(isScheduleConflict(a, b)).toBe(true)
  })

  it("returns false for adjacent non-overlapping on same day", () => {
    const a = { day: "周一", start: "14:00", end: "15:30" }
    const b = { day: "周一", start: "15:30", end: "17:00" }
    expect(isScheduleConflict(a, b)).toBe(false)
  })

  it("normalizes day before comparison (周一 vs Mon)", () => {
    const a = { day: "周一", start: "14:00", end: "15:30" }
    const b = { day: "Mon", start: "15:00", end: "16:00" }
    expect(isScheduleConflict(a, b)).toBe(true)
  })

  it("normalizes day before comparison (Monday vs 星期一)", () => {
    const a = { day: "Monday", start: "14:00", end: "15:30" }
    const b = { day: "星期一", start: "15:00", end: "16:00" }
    expect(isScheduleConflict(a, b)).toBe(true)
  })

  it("returns false for non-overlapping on same day", () => {
    const a = { day: "周一", start: "08:00", end: "09:00" }
    const b = { day: "周一", start: "14:00", end: "15:30" }
    expect(isScheduleConflict(a, b)).toBe(false)
  })

  it("handles contained intervals", () => {
    const a = { day: "周一", start: "14:00", end: "17:00" }
    const b = { day: "周一", start: "15:00", end: "16:00" }
    expect(isScheduleConflict(a, b)).toBe(true)
  })
})

describe("buildLotteryRankCase", () => {
  it("returns SQL object for empty input", () => {
    const sql = buildLotteryRankCase([], 1)
    expect(sql).toBeDefined()
  })

  it("returns SQL object for non-empty input", () => {
    const sql = buildLotteryRankCase(["id1", "id2", "id3"], 5)
    expect(sql).toBeDefined()
  })

  it("preserves order and start rank", () => {
    // SQL 对象本身不可直接断言内部结构，但应能成功构建
    const ids = ["a", "b", "c", "d"]
    const sql = buildLotteryRankCase(ids, 10)
    expect(sql).toBeDefined()
    // 不抛异常即视为通过；实际 SQL 执行由集成测试覆盖
  })
})

describe("mapCourseRow", () => {
  // 构造一个最小的 mock 行（仅包含 mapCourseRow 使用的字段）
  const mockRow: CourseCoreRow = {
    id: "course-1",
    name: "Python 入门",
    subjectId: "subj-1",
    teacherId: "user-1",
    gradeId: "grade-1",
    description: "Python 编程基础",
    capacity: 30,
    enrolledCount: 25,
    classroom: "A101",
    schedule: "周一 14:00-15:30",
    startDate: new Date("2026-09-01"),
    endDate: new Date("2027-01-15"),
    selectionStartAt: new Date("2026-08-20T08:00:00Z"),
    selectionEndAt: new Date("2026-08-25T18:00:00Z"),
    status: "open",
    selectionMode: "lottery",
    credit: "2.0",
    createdAt: new Date("2026-06-01T00:00:00Z"),
    updatedAt: new Date("2026-06-15T12:00:00Z"),
  }

  it("maps basic fields correctly", () => {
    const result = mapCourseRow(
      mockRow,
      new Map(),
      new Map(),
      new Map()
    )
    expect(result.id).toBe("course-1")
    expect(result.name).toBe("Python 入门")
    expect(result.capacity).toBe(30)
    expect(result.enrolledCount).toBe(25)
    expect(result.status).toBe("open")
    expect(result.selectionMode).toBe("lottery")
  })

  it("converts credit to string", () => {
    const result = mapCourseRow(mockRow, new Map(), new Map(), new Map())
    expect(result.credit).toBe("2.0")
    expect(typeof result.credit).toBe("string")
  })

  it("formats startDate as YYYY-MM-DD", () => {
    const result = mapCourseRow(mockRow, new Map(), new Map(), new Map())
    expect(result.startDate).toBe("2026-09-01")
  })

  it("converts timestamps to ISO strings", () => {
    const result = mapCourseRow(mockRow, new Map(), new Map(), new Map())
    expect(result.createdAt).toBe("2026-06-01T00:00:00.000Z")
    expect(result.updatedAt).toBe("2026-06-15T12:00:00.000Z")
    expect(result.selectionStartAt).toBe("2026-08-20T08:00:00.000Z")
  })

  it("resolves teacherName from map", () => {
    const teacherNames = new Map([["user-1", "张老师"]])
    const result = mapCourseRow(mockRow, teacherNames, new Map(), new Map())
    expect(result.teacherName).toBe("张老师")
  })

  it("returns null teacherName when teacherId not in map", () => {
    const result = mapCourseRow(mockRow, new Map(), new Map(), new Map())
    expect(result.teacherName).toBeNull()
  })

  it("resolves subjectName and gradeName from maps", () => {
    const teacherNames = new Map([["user-1", "张老师"]])
    const subjectNames = new Map([["subj-1", "计算机"]])
    const gradeNames = new Map([["grade-1", "高一"]])
    const result = mapCourseRow(mockRow, teacherNames, subjectNames, gradeNames)
    expect(result.subjectName).toBe("计算机")
    expect(result.gradeName).toBe("高一")
  })

  it("returns null subjectName/gradeName when ids are null", () => {
    const rowWithNulls: CourseCoreRow = {
      ...mockRow,
      subjectId: null,
      gradeId: null,
    }
    const result = mapCourseRow(rowWithNulls, new Map(), new Map(), new Map())
    expect(result.subjectName).toBeNull()
    expect(result.gradeName).toBeNull()
    expect(result.subjectId).toBeNull()
    expect(result.gradeId).toBeNull()
  })
})
