import { afterEach, describe, expect, it } from 'vitest'
import { watch, nextTick } from 'vue'
import {
  createTerminalApp,
  createStdoutRenderer,
} from '@simon_he/vue-tui'
import {
  createDimSdkRouter,
  DimSdkShell,
} from '../src/dimsdk/index.ts'
import { createErrorGoatChainAgent as createErrorDimSdkAgent } from '../src/dimsdk/core/agents/error.ts'
import {
  createUiThemeSwitcher,
  DimSdkUiThemeKey,
} from '../src/dimsdk/ui/theme/index.ts'

async function flushApp(
  app: Readonly<{
    scheduler: { flush: () => void }
  }>,
  ticks = 6,
): Promise<void> {
  for (let index = 0; index < ticks; index += 1) {
    await nextTick()
    await Promise.resolve()
  }
  app.scheduler.flush()
}

async function waitFor(
  predicate: () => boolean,
  advance: () => Promise<void>,
  timeoutMs = 1500,
): Promise<void> {
  const start = Date.now()
  while (Date.now() - start < timeoutMs) {
    if (predicate())
      return
    await advance()
  }
  throw new Error('waitFor timeout')
}

function styleMaskFromAnsi(
  output: string,
  cols: number,
  rows: number,
): readonly string[] {
  const grid = Array.from(
    { length: rows },
    () => Array.from({ length: cols }, () => '-/black////'),
  )
  let cursorX = 0
  let cursorY = 0
  let scrollTop = 0
  let scrollBottom = rows
  let current = {
    fg: '-' as string,
    bg: 'black',
    bold: false,
    dim: false,
    inverse: false,
    underline: false,
  }

  const encode = () =>
    [
      current.fg,
      current.bg,
      current.bold ? 'b' : '',
      current.dim ? 'd' : '',
      current.inverse ? 'i' : '',
      current.underline ? 'u' : '',
    ].join('/')

  const reset = () => {
    current = {
      fg: '-',
      bg: 'black',
      bold: false,
      dim: false,
      inverse: false,
      underline: false,
    }
  }

  const fgName = (code: number): string | null => {
    const names = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']
    if (code >= 30 && code <= 37)
      return names[code - 30]!
    if (code >= 90 && code <= 97)
      return `${names[code - 90]!}Bright`
    return null
  }

  const bgName = (code: number): string | null => {
    const names = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']
    if (code >= 40 && code <= 47)
      return names[code - 40]!
    if (code >= 100 && code <= 107)
      return `${names[code - 100]!}Bright`
    return null
  }

  const scrollUp = (count: number) => {
    for (let i = 0; i < count; i++) {
      for (let y = scrollTop; y < scrollBottom - 1; y++)
        grid[y] = grid[y + 1]!
      grid[scrollBottom - 1] = Array.from({ length: cols }, () => '-/black////')
    }
  }

  const scrollDown = (count: number) => {
    for (let i = 0; i < count; i++) {
      for (let y = scrollBottom - 1; y > scrollTop; y--)
        grid[y] = grid[y - 1]!
      grid[scrollTop] = Array.from({ length: cols }, () => '-/black////')
    }
  }

  let i = 0
  while (i < output.length) {
    const ch = output[i]!
    if (ch === '\u001B') {
      const next = output[i + 1]
      if (next === '[') {
        let j = i + 2
        while (j < output.length && !/[A-Za-z]/.test(output[j]!))
          j++
        if (j >= output.length)
          break
        const final = output[j]!
        const raw = output.slice(i + 2, j)
        const params = raw.replace(/^\?/, '')
        const parts = params
          ? params.split(';').map(part => Number(part || '0'))
          : []
        if (final === 'H' || final === 'f') {
          cursorY = Math.max(0, Math.min(rows - 1, (parts[0] || 1) - 1))
          cursorX = Math.max(0, Math.min(cols, (parts[1] || 1) - 1))
        }
        else if (final === 'J') {
          const mode = parts[0] || 0
          const style = encode()
          if (mode === 2) {
            for (let y = 0; y < rows; y++) {
              for (let x = 0; x < cols; x++)
                grid[y]![x] = style
            }
          }
        }
        else if (final === 'K') {
          const style = encode()
          for (let x = cursorX; x < cols; x++)
            grid[cursorY]![x] = style
        }
        else if (final === 'r') {
          if (parts.length >= 2) {
            scrollTop = Math.max(0, Math.min(rows - 1, (parts[0] || 1) - 1))
            scrollBottom = Math.max(
              scrollTop + 1,
              Math.min(rows, parts[1] || rows),
            )
          }
          else {
            scrollTop = 0
            scrollBottom = rows
          }
        }
        else if (final === 'S') {
          scrollUp(Math.max(1, parts[0] || 1))
        }
        else if (final === 'T') {
          scrollDown(Math.max(1, parts[0] || 1))
        }
        else if (final === 'm') {
          if (parts.length === 0)
            reset()
          for (const part of parts) {
            if (part === 0)
              reset()
            else if (part === 1)
              current.bold = true
            else if (part === 2)
              current.dim = true
            else if (part === 4)
              current.underline = true
            else if (part === 7)
              current.inverse = true
            else if (part === 22) {
              current.bold = false
              current.dim = false
            }
            else if (part === 24)
              current.underline = false
            else if (part === 27)
              current.inverse = false
            else if (part === 39)
              current.fg = '-'
            else if (part === 49)
              current.bg = 'black'
            else if (fgName(part))
              current.fg = fgName(part)!
            else if (bgName(part))
              current.bg = bgName(part)!
          }
        }
        i = j + 1
        continue
      }
      if (next === ']') {
        const end = output.indexOf('\u0007', i + 2)
        i = end >= 0 ? end + 1 : output.length
        continue
      }
    }

    if (ch === '\r') {
      cursorX = 0
      i++
      continue
    }
    if (ch === '\n') {
      cursorX = 0
      if (cursorY === scrollBottom - 1)
        scrollUp(1)
      else
        cursorY = Math.min(rows - 1, cursorY + 1)
      i++
      continue
    }

    const style = encode()
    if (cursorY >= 0 && cursorY < rows && cursorX >= 0 && cursorX < cols)
      grid[cursorY]![cursorX] = style
    cursorX = Math.min(cols, cursorX + 1)
    i++
  }

  return grid.map(row => row.join('|'))
}

function cellStyleFromAnsi(
  output: string,
  cols: number,
  rows: number,
  x: number,
  y: number,
): string {
  const row = styleMaskFromAnsi(output, cols, rows)[y] ?? ''
  return row.split('|')[x] ?? ''
}

async function mountHomeThemeRuntime(
  opts: Readonly<{ cols?: number, rows?: number }> = {},
) {
  const cols = opts.cols ?? 100
  const rows = opts.rows ?? 36
  let storeRef: any

  const cap = {
    mode: 'ansi16',
    level: 16,
  } as const
  const uiThemeSwitcher = createUiThemeSwitcher({
    mode: 'monokai-dark',
    capability: cap,
  })
  const uiTheme = uiThemeSwitcher.context
  let pendingThemeUpdate: {
    defaultStyle?: any
    defaultBg?: any
    palette?: any
  } | null = null
  let applyThemeUpdate:
    | ((next: { defaultStyle?: any, defaultBg?: any, palette?: any }) => void)
    | null = null

  const app = createTerminalApp({
    cols,
    rows,
    component: DimSdkShell as never,
    props: {
      toolRunner: async () => ({
        id: 'tool',
        status: 'error' as const,
        output: 'Tool execution is disabled in this test.',
      }),
      context: {
        system: 'test',
        cwd: process.cwd(),
        env: {
          ...process.env,
          DIMCODE_VERSION: process.env.DIMCODE_VERSION ?? '1.0.0',
        },
      },
      agent: createErrorDimSdkAgent(new Error('Agent is disabled in this test.')),
      onStoreCreated: (store: any) => {
        storeRef = store
        watch(
          () => store.state.ui.theme.mode,
          (mode) => {
            uiThemeSwitcher.setMode(mode as any)
            const update = {
              defaultStyle: uiTheme.style(['text.primary', 'bg.window']),
              defaultBg: (uiTheme.token('bg.window')?.bg ?? 'black') as any,
              palette: uiTheme.palette ?? undefined,
            }
            if (applyThemeUpdate)
              applyThemeUpdate(update)
            else
              pendingThemeUpdate = update
          },
          { immediate: true },
        )
      },
    },
    defaultStyle: uiTheme.style(['text.primary', 'bg.window']),
  })

  app.app.use(createDimSdkRouter({ initialRoute: 'home', mode: 'cli' }) as never)
  app.app.provide(DimSdkUiThemeKey, uiTheme)
  app.mount()
  await flushApp(app)
  await waitFor(() => Boolean(storeRef), () => flushApp(app))

  const ansiOutput = { text: '' }
  const stdoutRenderer = createStdoutRenderer(app.terminal as any, {
    output: {
      isTTY: true,
      write(chunk: string) {
        ansiOutput.text += chunk
      },
    },
    clear: false,
    hideCursor: false,
    altScreen: false,
    defaultBg: (uiTheme.token('bg.window')?.bg ?? 'black') as any,
    palette: uiTheme.palette ?? undefined,
  })

  applyThemeUpdate = (next) => {
    if (next.defaultStyle)
      app.defaultStyle.value = next.defaultStyle
    stdoutRenderer.updateTheme?.({
      defaultBg: next.defaultBg,
      palette: next.palette,
    })
    app.scheduler.invalidate()
    app.scheduler.flush()
    stdoutRenderer.render()
  }
  if (pendingThemeUpdate) {
    applyThemeUpdate(pendingThemeUpdate)
    pendingThemeUpdate = null
  }

  await flushApp(app)

  return {
    app,
    store: storeRef,
    ansiOutput,
    dispose() {
      stdoutRenderer.dispose()
      app.dispose()
    },
  }
}

afterEach(() => {
  process.env.DIMCODE_TEST = '1'
})

describe('dimsdk theme runtime regression', () => {
  it('repaints the home surface to light window background after switching to monokai light', async () => {
    const mounted = await mountHomeThemeRuntime()

    try {
      expect(mounted.app.terminal.getCell(0, 0).style.bg).toBe('black')

      mounted.ansiOutput.text = ''
      mounted.store.state.ui.theme.mode = 'monokai-light'

      await waitFor(
        () => mounted.app.terminal.getCell(0, 0).style.bg === 'white',
        () => flushApp(mounted.app, 10),
      )
      expect(mounted.app.terminal.getCell(0, 0).style.bg).toBe('white')
    }
    catch (error) {
      console.log('top-left style after switch:', cellStyleFromAnsi(mounted.ansiOutput.text, 100, 36, 0, 0))
      console.log('ansi tail:', JSON.stringify(mounted.ansiOutput.text.slice(-160)))
      console.log('cell00 terminal style:', mounted.app.terminal.getCell(0, 0).style)
      throw error
    }
    finally {
      mounted.dispose()
    }
  })
})
