import { describe, it, expect } from "bun:test"
import {
  fileName,
  dirName,
  buildHighlightSegments,
  atEnd,
  insertSpacedText,
  isPromptBlocked,
  isPromptBusy,
  isSuggesting,
  isQuestioning,
  isPathMention,
  optionPeriodEdit,
  optionPeriodInput,
  canRestoreOptionPeriodEdit,
} from "../../webview-ui/src/components/chat/prompt-input-utils"

describe("fileName", () => {
  it("extracts the last segment of a unix path", () => {
    expect(fileName("src/components/chat/PromptInput.tsx")).toBe("PromptInput.tsx")
  })

  it("extracts the last segment of a Windows path", () => {
    expect(fileName("src\\components\\chat\\PromptInput.tsx")).toBe("PromptInput.tsx")
  })

  it("returns the path itself when no separator present", () => {
    expect(fileName("README.md")).toBe("README.md")
  })

  it("returns the filename for a single directory segment", () => {
    expect(fileName("src/foo.ts")).toBe("foo.ts")
  })

  it("handles mixed separators", () => {
    expect(fileName("src\\components/chat/File.tsx")).toBe("File.tsx")
  })
})

describe("dirName", () => {
  it("returns empty string for a file with no directory", () => {
    expect(dirName("README.md")).toBe("")
  })

  it("returns the directory for a simple path", () => {
    expect(dirName("src/foo.ts")).toBe("src")
  })

  it("returns full directory for a short path", () => {
    expect(dirName("src/components/foo.ts")).toBe("src/components")
  })

  it("truncates long directories to last two segments", () => {
    const path = "packages/kilo-vscode/webview-ui/src/components/chat/foo.ts"
    const result = dirName(path)
    expect(result).toMatch(/^…\//)
    expect(result).toContain("components/chat")
  })

  it("does not truncate directories at exactly 30 chars", () => {
    const dir = "a".repeat(15) + "/" + "b".repeat(14)
    const result = dirName(`${dir}/file.ts`)
    expect(result).toBe(dir)
  })

  it("truncates directories longer than 30 chars", () => {
    const dir = "a".repeat(16) + "/" + "b".repeat(15)
    const result = dirName(`${dir}/file.ts`)
    expect(result.startsWith("…/")).toBe(true)
  })

  it("normalizes Windows backslashes before measuring length", () => {
    const result = dirName("src\\foo.ts")
    expect(result).toBe("src")
  })
})

describe("buildHighlightSegments", () => {
  it("returns single non-highlighted segment when paths set is empty", () => {
    const result = buildHighlightSegments("hello world", new Set())
    expect(result).toEqual([{ text: "hello world", highlight: false }])
  })

  it("returns single non-highlighted segment when no mention present", () => {
    const result = buildHighlightSegments("hello world", new Set(["foo.ts"]))
    expect(result).toEqual([{ text: "hello world", highlight: false }])
  })

  it("highlights a single mention token", () => {
    const result = buildHighlightSegments("@foo.ts", new Set(["foo.ts"]))
    expect(result).toEqual([{ text: "@foo.ts", highlight: true }])
  })

  it("splits text before and highlight token", () => {
    const result = buildHighlightSegments("see @foo.ts here", new Set(["foo.ts"]))
    expect(result).toEqual([
      { text: "see ", highlight: false },
      { text: "@foo.ts", highlight: true },
      { text: " here", highlight: false },
    ])
  })

  it("highlights multiple mentions in order", () => {
    const result = buildHighlightSegments("@a.ts and @b.ts done", new Set(["a.ts", "b.ts"]))
    expect(result).toEqual([
      { text: "@a.ts", highlight: true },
      { text: " and ", highlight: false },
      { text: "@b.ts", highlight: true },
      { text: " done", highlight: false },
    ])
  })

  it("picks the earliest mention when multiple paths could match", () => {
    const result = buildHighlightSegments("@b.ts then @a.ts", new Set(["a.ts", "b.ts"]))
    expect(result[0]).toEqual({ text: "@b.ts", highlight: true })
    expect(result[2]).toEqual({ text: "@a.ts", highlight: true })
  })

  it("handles back-to-back mentions with no separator", () => {
    const result = buildHighlightSegments("@a.ts@b.ts", new Set(["a.ts", "b.ts"]))
    const highlighted = result.filter((s) => s.highlight)
    expect(highlighted).toHaveLength(2)
  })

  it("returns empty array for empty string", () => {
    const result = buildHighlightSegments("", new Set(["foo.ts"]))
    expect(result).toEqual([])
  })

  it("does not partially match longer paths", () => {
    const result = buildHighlightSegments("@foo.ts", new Set(["foo.tsx"]))
    expect(result).toEqual([{ text: "@foo.ts", highlight: false }])
  })
})

describe("atEnd", () => {
  it("returns true when caret is at end with no selection", () => {
    expect(atEnd(5, 5, 5)).toBe(true)
  })

  it("returns false when caret is before end", () => {
    expect(atEnd(4, 4, 5)).toBe(false)
  })

  it("returns false when there is a selection", () => {
    expect(atEnd(2, 5, 5)).toBe(false)
  })

  it("returns true for empty input", () => {
    expect(atEnd(0, 0, 0)).toBe(true)
  })

  it("returns false when caret is at start of non-empty input", () => {
    expect(atEnd(0, 0, 10)).toBe(false)
  })
})

describe("Option+Period input", () => {
  const key = {
    code: "Period",
    altKey: true,
    ctrlKey: false,
    metaKey: false,
    shiftKey: false,
    isComposing: false,
  }

  it("records the value and selection before native input", () => {
    expect(optionPeriodEdit(key, "hello", 1, 4, "backward")).toEqual({
      before: "hello",
      start: 1,
      end: 4,
      direction: "backward",
    })
  })

  it("rejects modified, composing, and non-period keys", () => {
    expect(optionPeriodEdit({ ...key, code: "Comma" }, "", 0, 0, "none")).toBeUndefined()
    expect(optionPeriodEdit({ ...key, ctrlKey: true }, "", 0, 0, "none")).toBeUndefined()
    expect(optionPeriodEdit({ ...key, metaKey: true }, "", 0, 0, "none")).toBeUndefined()
    expect(optionPeriodEdit({ ...key, shiftKey: true }, "", 0, 0, "none")).toBeUndefined()
    expect(optionPeriodEdit({ ...key, isComposing: true }, "", 0, 0, "none")).toBeUndefined()
  })

  it.each([
    ["≥", "he≥llo", 3],
    ["…", "he…llo", 3],
    ["🙂", "he🙂llo", 4],
  ])("derives the native insertion from the input event for %s", (data, value, pos) => {
    const edit = optionPeriodEdit(key, "hello", 2, 2, "none")!
    expect(optionPeriodInput(edit, { inputType: "insertText", data, isComposing: false }, value, pos, pos)).toEqual({
      ...edit,
      after: value,
      pos,
    })
  })

  it("records replacement of selected text", () => {
    const edit = optionPeriodEdit(key, "hello", 1, 4, "backward")!
    expect(optionPeriodInput(edit, { inputType: "insertText", data: "…", isComposing: false }, "h…o", 2, 2)).toEqual({
      ...edit,
      after: "h…o",
      pos: 2,
    })
  })

  it("rejects input that does not exactly match the native edit", () => {
    const edit = optionPeriodEdit(key, "a≥b", 3, 3, "none")!
    expect(
      optionPeriodInput(edit, { inputType: "insertFromPaste", data: "≥", isComposing: false }, "a≥b≥", 4, 4),
    ).toBeUndefined()
    expect(
      optionPeriodInput(edit, { inputType: "insertCompositionText", data: "≥", isComposing: false }, "a≥b≥", 4, 4),
    ).toBeUndefined()
    expect(
      optionPeriodInput(edit, { inputType: "insertText", data: "≥", isComposing: true }, "a≥b≥", 4, 4),
    ).toBeUndefined()
    expect(
      optionPeriodInput(edit, { inputType: "insertText", data: null, isComposing: false }, "a≥b", 3, 3),
    ).toBeUndefined()
    expect(
      optionPeriodInput(edit, { inputType: "insertText", data: "", isComposing: false }, "a≥b", 3, 3),
    ).toBeUndefined()
    expect(
      optionPeriodInput(edit, { inputType: "insertText", data: "≥", isComposing: false }, "a≥b≥x", 5, 5),
    ).toBeUndefined()
    expect(
      optionPeriodInput(edit, { inputType: "insertText", data: "≥", isComposing: false }, "a≥b≥", 3, 4),
    ).toBeUndefined()
  })

  it("restores only while the observed value and caret still match", () => {
    const edit = optionPeriodEdit(key, "a≥b", 3, 3, "none")!
    const input = optionPeriodInput(edit, { inputType: "insertText", data: "🙂", isComposing: false }, "a≥b🙂", 5, 5)!
    expect(canRestoreOptionPeriodEdit(input, "a≥b🙂", 5, 5)).toBe(true)
    expect(canRestoreOptionPeriodEdit(input, "a≥b🙂x", 6, 6)).toBe(false)
    expect(canRestoreOptionPeriodEdit(input, "a≥b🙂", 4, 4)).toBe(false)
  })
})

describe("isPromptBlocked", () => {
  it("returns false when zero permissions", () => {
    expect(isPromptBlocked(0)).toBe(false)
  })

  it("returns true when permissions exist", () => {
    expect(isPromptBlocked(1)).toBe(true)
    expect(isPromptBlocked(3)).toBe(true)
  })

  it("accepts exactly one argument (locks the API against regression)", () => {
    // Prevents a future change from reintroducing the question/blocking coupling.
    // See prompt-send-contract.test.ts for the source-level complement.
    expect(isPromptBlocked.length).toBe(1)
  })
})

describe("isPromptBusy", () => {
  it("returns true when busy and neither suggesting nor questioning", () => {
    expect(isPromptBusy("busy", false, false, false)).toBe(true)
  })

  it("returns true while submitting before the backend reports busy", () => {
    expect(isPromptBusy("idle", false, false, true)).toBe(true)
  })

  it("returns false when idle regardless of suggesting/questioning", () => {
    expect(isPromptBusy("idle", false, false, false)).toBe(false)
    expect(isPromptBusy("idle", true, false, false)).toBe(false)
    expect(isPromptBusy("idle", false, true, false)).toBe(false)
    expect(isPromptBusy("idle", true, true, false)).toBe(false)
  })

  it("returns false when busy but suggesting is true (suggestion decoupling)", () => {
    expect(isPromptBusy("busy", true, false, false)).toBe(false)
  })

  it("returns false when busy but questioning is true (question decoupling)", () => {
    expect(isPromptBusy("busy", false, true, false)).toBe(false)
  })

  it("returns false when busy and both suggesting and questioning", () => {
    expect(isPromptBusy("busy", true, true, false)).toBe(false)
  })

  it("returns true for non-idle non-busy status when not suggesting/questioning", () => {
    expect(isPromptBusy("retry", false, false, false)).toBe(true)
  })
})

describe("insertSpacedText", () => {
  it("inserts transcript into empty text", () => {
    expect(insertSpacedText("", "hello", 0, 0)).toEqual({ text: "hello", pos: 5 })
  })

  it("adds spaces between surrounding words", () => {
    expect(insertSpacedText("helloworld", "beautiful", 5, 5)).toEqual({ text: "hello beautiful world", pos: 16 })
  })

  it("does not duplicate existing spaces", () => {
    expect(insertSpacedText("hello world", "beautiful", 6, 6)).toEqual({ text: "hello beautiful world", pos: 16 })
  })

  it("replaces selected text and keeps caret after transcript", () => {
    expect(insertSpacedText("hello bad world", "beautiful", 6, 9)).toEqual({ text: "hello beautiful world", pos: 15 })
  })

  it("preserves leading and trailing insertion positions", () => {
    expect(insertSpacedText("world", "hello", 0, 0)).toEqual({ text: "hello world", pos: 6 })
    expect(insertSpacedText("hello", "world", 5, 5)).toEqual({ text: "hello world", pos: 11 })
  })
})

describe("isSuggesting", () => {
  it("returns true when not blocked and suggestions > 0", () => {
    expect(isSuggesting(false, 1)).toBe(true)
    expect(isSuggesting(false, 3)).toBe(true)
  })

  it("returns false when blocked even with suggestions", () => {
    expect(isSuggesting(true, 2)).toBe(false)
  })

  it("returns false when not blocked but no suggestions", () => {
    expect(isSuggesting(false, 0)).toBe(false)
  })
})

describe("isQuestioning", () => {
  it("returns true when not blocked and questions > 0", () => {
    expect(isQuestioning(false, 1)).toBe(true)
    expect(isQuestioning(false, 5)).toBe(true)
  })

  it("returns false when blocked even with questions", () => {
    expect(isQuestioning(true, 2)).toBe(false)
  })

  it("returns false when not blocked but no questions", () => {
    expect(isQuestioning(false, 0)).toBe(false)
  })
})

describe("isPathMention", () => {
  it("returns true for a file path", () => {
    expect(isPathMention("@src/foo.ts")).toBe(true)
  })

  it("returns true for a simple filename", () => {
    expect(isPathMention("@README.md")).toBe(true)
  })

  it("returns true for a folder path with trailing slash", () => {
    expect(isPathMention("@src/components/")).toBe(true)
  })

  it("returns true for a folder path without trailing slash", () => {
    expect(isPathMention("@src/components")).toBe(true)
  })

  it("returns false for terminal mention", () => {
    expect(isPathMention("@terminal")).toBe(false)
  })

  it("returns false for git-changes mention", () => {
    expect(isPathMention("@git-changes")).toBe(false)
  })

  it("handles text without @ prefix", () => {
    expect(isPathMention("src/foo.ts")).toBe(true)
  })
})
