import { describe, expect, it, vi } from "vitest"
import { SearchService } from "./search"

function makeService(options: {
  ftsResults?: { block_id: string }[]
  embRows?: { id: string; embedding: ArrayBuffer }[]
  contentRows?: { id: string; content: string; page_path: string }[]
  titleRows?: { path: string; title: string }[]
  queryVec?: number[]
  ranked?: { block_id: string; score: number }[]
}) {
  const db = {
    select: vi.fn().mockImplementation((sql: string) => {
      if (sql.includes("blocks_fts"))
        return Promise.resolve(options.ftsResults ?? [])
      if (sql.includes("embedding IS NOT NULL"))
        return Promise.resolve(options.embRows ?? [])
      if (sql.includes("WHERE id IN"))
        return Promise.resolve(options.contentRows ?? [])
      if (sql.includes("WHERE path IN"))
        return Promise.resolve(options.titleRows ?? [])
      return Promise.resolve([])
    }),
  } as any

  const invoke = vi.fn().mockImplementation(async (cmd: string) => {
    if (cmd === "compute_embedding") return options.queryVec ?? []
    if (cmd === "rank_by_similarity") return options.ranked ?? []
    return []
  })

  return new SearchService(db, "/workspace", invoke)
}

describe("SearchService", () => {
  it("returns empty for empty query", async () => {
    const svc = makeService({})
    expect(await svc.search("")).toEqual([])
    expect(await svc.search("  ")).toEqual([])
  })

  it("returns keyword-only results when vector search is unavailable", async () => {
    const svc = makeService({
      ftsResults: [{ block_id: "b1" }, { block_id: "b2" }],
      contentRows: [
        { id: "b1", content: "hello world", page_path: "pages/Foo.md" },
        { id: "b2", content: "goodbye world", page_path: "pages/Bar.md" },
      ],
      titleRows: [
        { path: "pages/Foo.md", title: "Foo" },
        { path: "pages/Bar.md", title: "Bar" },
      ],
    })

    const results = await svc.search("hello")
    expect(results).toHaveLength(2)
    expect(results[0].source).toBe("keyword")
  })

  it("returns hybrid results when vector search succeeds", async () => {
    const svc = makeService({
      ftsResults: [{ block_id: "b1" }, { block_id: "b2" }],
      embRows: [
        {
          id: "b1",
          embedding: new Float32Array([0.1, 0.2]).buffer as ArrayBuffer,
        },
      ],
      queryVec: [0.1, 0.2],
      ranked: [{ block_id: "b1", score: 0.95 }],
      contentRows: [
        { id: "b1", content: "vector match", page_path: "pages/Vec.md" },
        { id: "b2", content: "keyword only", page_path: "pages/Key.md" },
      ],
      titleRows: [
        { path: "pages/Vec.md", title: "Vec" },
        { path: "pages/Key.md", title: "Key" },
      ],
    })

    const results = await svc.search("test")
    // b1 appears in both FTS and vector → hybrid; b2 appears only in FTS → keyword
    const b1 = results.find((r) => r.blockId === "b1")
    const b2 = results.find((r) => r.blockId === "b2")
    expect(b1).toBeDefined()
    expect(b2).toBeDefined()
    expect(b1?.source).toBe("hybrid")
    expect(b2?.source).toBe("keyword")
    // b1 (hybrid) should rank higher than b2 (keyword only)
    expect(results[0]?.blockId).toBe("b1")
  })

  it("returns empty when no results match", async () => {
    const svc = makeService({
      ftsResults: [],
    })
    expect(await svc.search("nonexistent")).toEqual([])
  })

  it("gracefully degrades from vector errors", async () => {
    const invoke = vi.fn().mockRejectedValue(new Error("model not loaded"))
    const db = {
      select: vi.fn().mockImplementation((sql: string) => {
        if (sql.includes("blocks_fts"))
          return Promise.resolve([{ block_id: "b1" }])
        if (sql.includes("WHERE id IN"))
          return Promise.resolve([
            { id: "b1", content: "hello world", page_path: "pages/Foo.md" },
          ])
        if (sql.includes("WHERE path IN"))
          return Promise.resolve([{ path: "pages/Foo.md", title: "Foo" }])
        return Promise.resolve([])
      }),
    } as any

    const svc = new SearchService(db, "/workspace", invoke)
    const results = await svc.search("hello")
    expect(results).toHaveLength(1)
    expect(results[0].source).toBe("keyword")
  })

  it("builds excerpts with context around the match", async () => {
    // 200 chars of content — enough that the excerpt should be shorter
    const longContent = Array.from({ length: 40 }, (_, i) => `word${i}`).join(
      " ",
    )
    const svc = makeService({
      ftsResults: [{ block_id: "b1" }],
      contentRows: [
        { id: "b1", content: longContent, page_path: "pages/Long.md" },
      ],
      titleRows: [{ path: "pages/Long.md", title: "Long" }],
    })

    const results = await svc.search("word20")
    expect(results).toHaveLength(1)
    expect(results[0].excerpt).toContain("word20")
    // Excerpt should be centered around the match, not the full content
    expect(results[0].excerpt.length).toBeLessThan(longContent.length)
  })

  it("enriches results with page titles", async () => {
    const svc = makeService({
      ftsResults: [{ block_id: "b1" }],
      contentRows: [
        { id: "b1", content: "some content", page_path: "pages/MyPage.md" },
      ],
      titleRows: [{ path: "pages/MyPage.md", title: "My Custom Title" }],
    })

    const results = await svc.search("some")
    expect(results[0].pageTitle).toBe("My Custom Title")
  })

  it("falls back to filename stem when page has no title", async () => {
    const svc = makeService({
      ftsResults: [{ block_id: "b1" }],
      contentRows: [
        { id: "b1", content: "content", page_path: "pages/Untitled.md" },
      ],
      titleRows: [{ path: "pages/Untitled.md", title: "" }],
    })

    const results = await svc.search("content")
    expect(results[0].pageTitle).toBe("Untitled")
  })
})
