import { TextSelection } from "prosemirror-state"
import type { EditorView } from "prosemirror-view"
import type { Ref } from "vue"
import type { EditorBlockData } from "@/lib/block-parser"
import { serializeBlocks } from "@/lib/block-parser"
import { inlineToString } from "@/lib/content-model"
import { moveBlock } from "@/lib/editor-operations"
import type { OperationHistory } from "@/lib/operation-history"

export interface TestUtilsDeps {
  testMode: boolean
  blocks: Ref<EditorBlockData[]>
  blockRefs: Map<
    string,
    { view?: EditorView; setContent?: (md: string) => void }
  >
  onBlockContentChange: () => void
  history: OperationHistory
  canUndo: Ref<boolean>
  canRedo: Ref<boolean>
}

/**
 * Inject window.__testUtils__ for Playwright E2E tests.
 * Calls the 11 methods:
 *   selectText, getBlockIds, insertText, replaceContent, getContent,
 *   focusAtEnd, focusAtStart, toggleBlur, getParseTree, reorderBlocks, setTimeSource
 */
export function injectTestUtils(deps: TestUtilsDeps) {
  if (!deps.testMode)
    return // biome-ignore lint/suspicious/noExplicitAny: deliberate global injection for E2E
  ;(window as any).__testUtils__ = {
    selectText(blockId: string, from: number, to: number) {
      const api = deps.blockRefs.get(blockId)
      if (!api?.view) return
      const v = api.view
      v.focus()

      const doc = v.state.doc
      let charPos = 0
      let pmFrom: number | null = null
      let pmTo: number | null = null
      doc.descendants((node, pos) => {
        if (!node.isTextblock) return true
        const textLen = inlineToString(node).length
        const contentStart = pos + 1
        const charEnd = charPos + textLen
        if (pmFrom === null && from < charEnd) {
          pmFrom = contentStart + Math.max(0, from - charPos)
        }
        if (pmTo === null && to <= charEnd) {
          pmTo = contentStart + Math.max(0, to - charPos)
        }
        charPos = charEnd + 2
        return pmFrom === null || pmTo === null
      })
      if (pmFrom === null) pmFrom = doc.content.size
      if (pmTo === null) pmTo = doc.content.size
      v.dispatch(
        v.state.tr.setSelection(TextSelection.create(doc, pmFrom, pmTo)),
      )
    },

    getBlockIds() {
      return deps.blocks.value.map((b) => b.id)
    },

    insertText(blockId: string, text: string) {
      const api = deps.blockRefs.get(blockId)
      if (!api?.view) return
      const v = api.view
      v.focus()
      v.dispatch(v.state.tr.insertText(text))
    },

    replaceContent(blockId: string, text: string) {
      const api = deps.blockRefs.get(blockId)
      if (!api?.setContent) {
        console.warn(
          `[Editor] replaceContent: block ${blockId} has no setContent method`,
        )
        return
      }
      api.setContent(text)
    },

    getContent() {
      return serializeBlocks(deps.blocks.value)
    },

    focusAtEnd(blockId: string) {
      const api = deps.blockRefs.get(blockId)
      if (!api?.view) return
      const v = api.view
      const end = v.state.doc.content.size
      v.focus()
      v.dispatch(
        v.state.tr
          .setSelection(TextSelection.create(v.state.doc, end, end))
          .scrollIntoView(),
      )
    },

    focusAtStart(blockId: string) {
      const api = deps.blockRefs.get(blockId)
      if (!api?.view) return
      const v = api.view
      v.focus()
      let start = 0
      v.state.doc.descendants((node, pos) => {
        if (node.isTextblock) {
          start = pos + 1
          return false
        }
      })
      v.dispatch(
        v.state.tr
          .setSelection(TextSelection.create(v.state.doc, start, start))
          .scrollIntoView(),
      )
    },

    toggleBlur() {
      return new Promise<void>((resolve) => {
        const btn = document.createElement("button")
        btn.style.position = "fixed"
        btn.style.top = "0"
        btn.style.left = "0"
        document.body.appendChild(btn)
        btn.focus()
        requestAnimationFrame(() => {
          document.body.removeChild(btn)
          resolve()
        })
      })
    },

    getParseTree() {
      const firstApi = deps.blockRefs.values().next().value
      if (!firstApi?.view) return ""
      return firstApi.view.state.doc.toString()
    },

    reorderBlocks(fromIndex: number, toIndex: number) {
      if (fromIndex === toIndex) return
      const moved = deps.blocks.value[fromIndex]
      if (!moved) return
      deps.blocks.value = moveBlock(deps.blocks.value, fromIndex, toIndex)
      void deps.history.execute({
        type: "move-block",
        fromIndex,
        toIndex,
        blockId: moved.id,
        content: moved.content,
        depth: moved.depth,
        timestamp: Date.now(),
      })
      deps.canUndo.value = deps.history.canUndo
      deps.canRedo.value = deps.history.canRedo
      deps.onBlockContentChange()
    },

    setTimeSource(fakeMs: number) {
      // biome-ignore lint/suspicious/noExplicitAny: test-time injection
      ;(window as any).__testTimeSource = fakeMs
    },
  }
}
