import { describe, it, expect } from "vitest"
import type { Chapter, KnowledgePoint } from "./types"
import {
  buildChapterTree,
  buildChapterIndex,
  findChapterParent,
  filterKnowledgePointsByChapter,
  normalizeOptional,
  sortChapters,
  highlightKnowledgePoints,
  hasCycleAfterAddingEdge,
} from "./utils"

// 测试辅助：构造最小合法 Chapter（补齐 createdAt/updatedAt 等必填字段）
const makeChapter = (over: Partial<Chapter> & Pick<Chapter, "id" | "title" | "textbookId">): Chapter => ({
  order: 0,
  parentId: null,
  content: null,
  createdAt: new Date(0),
  updatedAt: new Date(0),
  ...over,
})

describe("textbooks/utils", () => {
  describe("sortChapters", () => {
    it("should sort by order ascending", () => {
      const a = makeChapter({ id: "1", title: "A", order: 2, textbookId: "t1" })
      const b = makeChapter({ id: "2", title: "B", order: 1, textbookId: "t1" })
      expect(sortChapters(a, b)).toBeGreaterThan(0)
    })

    it("should fall back to title localeCompare when order equal", () => {
      const a = makeChapter({ id: "1", title: "Banana", order: 1, textbookId: "t1" })
      const b = makeChapter({ id: "2", title: "Apple", order: 1, textbookId: "t1" })
      expect(sortChapters(a, b)).toBeGreaterThan(0)
    })

    it("should treat null order as 0", () => {
      const a = makeChapter({ id: "1", title: "A", order: null, textbookId: "t1" })
      const b = makeChapter({ id: "2", title: "B", order: 5, textbookId: "t1" })
      expect(sortChapters(a, b)).toBeLessThan(0)
    })
  })

  describe("buildChapterTree", () => {
    it("should return empty array for empty input", () => {
      expect(buildChapterTree([])).toEqual([])
    })

    it("should build single root", () => {
      const rows: Chapter[] = [
        makeChapter({ id: "1", title: "Root", order: 0, textbookId: "t1" }),
      ]
      const tree = buildChapterTree(rows)
      expect(tree).toHaveLength(1)
      expect(tree[0].id).toBe("1")
      expect(tree[0].children).toEqual([])
    })

    it("should build nested tree", () => {
      const rows: Chapter[] = [
        makeChapter({ id: "1", title: "Root", order: 0, textbookId: "t1" }),
        makeChapter({ id: "2", title: "Child 1", order: 0, textbookId: "t1", parentId: "1" }),
        makeChapter({ id: "3", title: "Child 2", order: 1, textbookId: "t1", parentId: "1" }),
        makeChapter({ id: "4", title: "Grandchild", order: 0, textbookId: "t1", parentId: "2" }),
      ]
      const tree = buildChapterTree(rows)
      expect(tree).toHaveLength(1)
      expect(tree[0].children).toHaveLength(2)
      expect(tree[0].children[0].children).toHaveLength(1)
      expect(tree[0].children[0].children[0].id).toBe("4")
    })

    it("should handle orphan nodes (parentId points to non-existent)", () => {
      const rows: Chapter[] = [
        makeChapter({ id: "1", title: "Orphan", order: 0, textbookId: "t1", parentId: "nonexistent" }),
      ]
      const tree = buildChapterTree(rows)
      expect(tree).toHaveLength(1)
      expect(tree[0].id).toBe("1")
    })

    it("should sort children by order", () => {
      const rows: Chapter[] = [
        makeChapter({ id: "1", title: "Root", order: 0, textbookId: "t1" }),
        makeChapter({ id: "2", title: "B", order: 2, textbookId: "t1", parentId: "1" }),
        makeChapter({ id: "3", title: "A", order: 1, textbookId: "t1", parentId: "1" }),
      ]
      const tree = buildChapterTree(rows)
      expect(tree[0].children[0].id).toBe("3")
      expect(tree[0].children[1].id).toBe("2")
    })
  })

  describe("buildChapterIndex", () => {
    it("should index all nodes including nested", () => {
      const chapters: Chapter[] = [
        makeChapter({
          id: "1",
          title: "Root",
          order: 0,
          textbookId: "t1",
          children: [
            makeChapter({ id: "2", title: "Child", order: 0, textbookId: "t1", parentId: "1", children: [] }),
          ],
        }),
      ]
      const index = buildChapterIndex(chapters)
      expect(index.size).toBe(2)
      expect(index.get("1")?.title).toBe("Root")
      expect(index.get("2")?.title).toBe("Child")
    })

    it("should return empty map for empty input", () => {
      expect(buildChapterIndex([]).size).toBe(0)
    })
  })

  describe("findChapterParent", () => {
    it("should find direct parent", () => {
      const chapters: Chapter[] = [
        makeChapter({
          id: "1",
          title: "Root",
          order: 0,
          textbookId: "t1",
          children: [
            makeChapter({ id: "2", title: "Child", order: 0, textbookId: "t1", parentId: "1", children: [] }),
          ],
        }),
      ]
      const parent = findChapterParent(chapters, "2")
      expect(parent?.id).toBe("1")
    })

    it("should return null for root node", () => {
      const chapters: Chapter[] = [
        makeChapter({ id: "1", title: "Root", order: 0, textbookId: "t1", children: [] }),
      ]
      expect(findChapterParent(chapters, "1")).toBeNull()
    })

    it("should return null for non-existent id", () => {
      expect(findChapterParent([], "nonexistent")).toBeNull()
    })
  })

  describe("filterKnowledgePointsByChapter", () => {
    it("should return empty for null chapterId", () => {
      expect(filterKnowledgePointsByChapter([], null)).toEqual([])
    })

    it("should filter by chapterId", () => {
      const kps: KnowledgePoint[] = [
        { id: "1", name: "KP1", chapterId: "c1", level: 1, order: 0 },
        { id: "2", name: "KP2", chapterId: "c2", level: 1, order: 0 },
        { id: "3", name: "KP3", chapterId: "c1", level: 2, order: 0 },
      ]
      const result = filterKnowledgePointsByChapter(kps, "c1")
      expect(result).toHaveLength(2)
      expect(result[0].id).toBe("1")
    })
  })

  describe("normalizeOptional", () => {
    it("should return null for empty string", () => {
      expect(normalizeOptional("")).toBeNull()
    })

    it("should return null for whitespace-only string", () => {
      expect(normalizeOptional("   ")).toBeNull()
    })

    it("should return null for null/undefined", () => {
      expect(normalizeOptional(null)).toBeNull()
      expect(normalizeOptional(undefined)).toBeNull()
    })

    it("should trim and return non-empty string", () => {
      expect(normalizeOptional("  hello  ")).toBe("hello")
    })
  })

  describe("highlightKnowledgePoints", () => {
    const makeKp = (id: string, name: string): KnowledgePoint => ({
      id,
      name,
      chapterId: "c1",
      level: 1,
      order: 0,
    })

    it("should return content unchanged when no knowledge points", () => {
      expect(highlightKnowledgePoints("hello world", [])).toBe("hello world")
    })

    it("should return content unchanged when content is empty", () => {
      expect(highlightKnowledgePoints("", [makeKp("1", "test")])).toBe("")
    })

    it("should wrap single knowledge point name with link", () => {
      const result = highlightKnowledgePoints("学习物理很有趣", [makeKp("kp1", "物理")])
      expect(result).toBe("学习[物理](#kp-kp1)很有趣")
    })

    it("should match case-insensitively", () => {
      const result = highlightKnowledgePoints("Hello HELLO hello", [makeKp("kp1", "hello")])
      expect(result).toBe("[Hello](#kp-kp1) [HELLO](#kp-kp1) [hello](#kp-kp1)")
    })

    it("should match multiple occurrences", () => {
      const result = highlightKnowledgePoints("物理物理物理", [makeKp("kp1", "物理")])
      expect(result).toBe("[物理](#kp-kp1)[物理](#kp-kp1)[物理](#kp-kp1)")
    })

    it("should prioritize longest match (物理学 before 物理)", () => {
      const kps = [makeKp("kp1", "物理"), makeKp("kp2", "物理学")]
      const result = highlightKnowledgePoints("物理学是研究物理的学科", kps)
      // "物理学" 应被 kp2 匹配，独立的 "物理" 应被 kp1 匹配
      expect(result).toBe("[物理学](#kp-kp2)是研究[物理](#kp-kp1)的学科")
    })

    it("should handle multiple different knowledge points in one pass", () => {
      const kps = [makeKp("kp1", "数学"), makeKp("kp2", "物理")]
      const result = highlightKnowledgePoints("数学和物理都是科学", kps)
      expect(result).toBe("[数学](#kp-kp1)和[物理](#kp-kp2)都是科学")
    })

    it("should escape regex metacharacters in knowledge point names", () => {
      const result = highlightKnowledgePoints("计算 1+2 的结果", [makeKp("kp1", "1+2")])
      expect(result).toBe("计算 [1+2](#kp-kp1) 的结果")
    })

    it("should handle knowledge point names with parentheses", () => {
      const result = highlightKnowledgePoints("函数 f(x) 是映射", [makeKp("kp1", "f(x)")])
      expect(result).toBe("函数 [f(x)](#kp-kp1) 是映射")
    })
  })
})

describe("textbooks/utils - cycle detection", () => {
  it("should return false when adding edge to empty graph", () => {
    const edges: Array<[string, string]> = []
    expect(hasCycleAfterAddingEdge(edges, "a", "b")).toBe(false)
  })

  it("should detect direct cycle (a->b then b->a)", () => {
    const edges: Array<[string, string]> = [["a", "b"]]
    expect(hasCycleAfterAddingEdge(edges, "b", "a")).toBe(true)
  })

  it("should detect indirect cycle (a->b->c then c->a)", () => {
    const edges: Array<[string, string]> = [["a", "b"], ["b", "c"]]
    expect(hasCycleAfterAddingEdge(edges, "c", "a")).toBe(true)
  })

  it("should not detect cycle for independent chains", () => {
    const edges: Array<[string, string]> = [["a", "b"], ["c", "d"]]
    expect(hasCycleAfterAddingEdge(edges, "b", "c")).toBe(false)
  })

  it("should not detect cycle for diamond shape", () => {
    const edges: Array<[string, string]> = [["a", "b"], ["a", "c"], ["b", "d"], ["c", "d"]]
    expect(hasCycleAfterAddingEdge(edges, "d", "e")).toBe(false)
  })

  it("should detect self-loop as cycle (a->a)", () => {
    expect(hasCycleAfterAddingEdge([], "a", "a")).toBe(true)
  })

  it("should not detect cycle for duplicate edge (a->b already exists)", () => {
    const edges: Array<[string, string]> = [["a", "b"]]
    expect(hasCycleAfterAddingEdge(edges, "a", "b")).toBe(false)
  })

  it("should detect longer indirect cycle (a->b->c->d then d->a)", () => {
    const edges: Array<[string, string]> = [["a", "b"], ["b", "c"], ["c", "d"]]
    expect(hasCycleAfterAddingEdge(edges, "d", "a")).toBe(true)
  })

  it("should not detect cycle when adding edge to unrelated node", () => {
    const edges: Array<[string, string]> = [["a", "b"], ["b", "c"]]
    expect(hasCycleAfterAddingEdge(edges, "d", "e")).toBe(false)
  })
})
