import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'
import { z } from '@bpinternal/zui'

import { getClient, getZai, metadata } from './utils'
import { TableAdapter } from '../src/adapters/botpress-table'

type File = {
  path: string
  name: string
  content: string
}

const simpleFile: File = {
  path: 'src/hello.ts',
  name: 'hello.ts',
  content: `function greet(name: string) {
  console.log('Hello, ' + name)
}

greet('World')`,
}

const configFile: File = {
  path: 'config.json',
  name: 'config.json',
  content: `{
  "version": "1.0.0",
  "name": "test-app",
  "enabled": true
}`,
}

const packageFile: File = {
  path: 'package.json',
  name: 'package.json',
  content: `{
  "name": "my-app",
  "version": "0.1.0",
  "dependencies": {
    "react": "^18.0.0"
  }
}`,
}

describe('zai.patch', { timeout: 60_000 }, () => {
  const zai = getZai()

  it('patches a single file with simple instruction and verifies syntax', async () => {
    const result = await zai.patch([simpleFile], 'change the greeting message to say "Hi" instead of "Hello"').result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].path).toBe('src/hello.ts')
    expect(result.output[0].name).toBe('hello.ts')
    expect(result.output[0].content).toContain('Hi')
    expect(result.output[0].content).not.toContain('Hello')
    expect(result.output[0].content).toContain('greet')

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "function greet(name: string) {
        console.log('Hi, ' + name)
      }

      greet('World')"
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎=2|  console.log('Hi, ' + name)"`)
  })

  it('patches a single file by adding a line', async () => {
    const result = await zai
      .patch(
        [simpleFile],
        'add a complete JSDoc comment block (/** ... */) above the greet function that describes what it does'
      )
      .result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].content).toContain('/**')
    expect(result.output[0].content).toContain('*/')
    expect(result.output[0].content).toContain('function greet')

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "/**
       * Greets the provided name.
       */
      function greet(name: string) {
        console.log('Hello, ' + name)
      }

      greet('World')"
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`
      "◼︎=1|/**
       * Greets the provided name.
       */
      function greet(name: string) {"
    `)
  })

  it('patches a single file by deleting a line', async () => {
    const result = await zai.patch([simpleFile], 'remove the greet function call at the bottom').result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].content).toContain('function greet')
    expect(result.output[0].content).not.toContain("greet('World')")

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "function greet(name: string) {
        console.log('Hello, ' + name)
      }
      "
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎-005"`)
  })

  it('patches multiple files with a single instruction', async () => {
    const files = [configFile, packageFile]
    const result = await zai.patch(files, 'change version to 2.0.0 in all files').result()

    expect(result.output).toHaveLength(2)
    expect(result.output.find((f) => f.name === 'config.json')?.content).toContain('"version": "2.0.0"')
    expect(result.output.find((f) => f.name === 'package.json')?.content).toContain('"version": "2.0.0"')

    expect(result.output.find((f) => f.name === 'config.json')?.patch).toMatchInlineSnapshot(
      `"◼︎=2|  "version": "2.0.0","`
    )
    expect(result.output.find((f) => f.name === 'package.json')?.patch).toMatchInlineSnapshot(
      `"◼︎=3|  "version": "2.0.0","`
    )
  })

  it('patches JSON file correctly', async () => {
    const result = await zai
      .patch([configFile], 'add a new field "description": "A test application" after the name field')
      .result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].content).toContain('"description": "A test application"')
    expect(result.output[0].content).toContain('"name": "test-app"')
    // Verify it's still valid JSON structure
    expect(() => JSON.parse(result.output[0].content)).not.toThrow()

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "{
        "version": "1.0.0",
        "name": "test-app",
        "description": "A test application",
        "enabled": true
      }"
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎>003|  "description": "A test application","`)
  })

  it('handles complex multi-line changes', async () => {
    const result = await zai
      .patch(
        [simpleFile],
        'refactor the greet function to accept an optional greeting parameter that defaults to "Hello"'
      )
      .result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].content).toContain('greet')
    // Should have parameter with default or optional parameter
    const hasDefaultParam =
      result.output[0].content.includes('greeting = ') ||
      result.output[0].content.includes('greeting?') ||
      result.output[0].content.includes('greeting: string = ')
    expect(hasDefaultParam).toBe(true)

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "function greet(name: string, greeting: string = "Hello") {
        console.log(greeting + ', ' + name)
      }

      greet('World')"
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`
      "◼︎=001|function greet(name: string, greeting: string = "Hello") {
      ◼︎=002|  console.log(greeting + ', ' + name)"
    `)
  })

  it('leaves files unchanged when instruction says no changes needed', async () => {
    const result = await zai.patch([simpleFile], 'only modify files that use React hooks').result()

    expect(result.output).toHaveLength(1)
    // File should be mostly unchanged since it doesn't use React
    expect(result.output[0].path).toBe(simpleFile.path)

    expect(result.output[0].patch).toMatchInlineSnapshot(`""`)
  })

  it('handles empty files array', async () => {
    const result = await zai.patch([], 'change something').result()

    expect(result.output).toHaveLength(0)
  })

  it('preserves file metadata (path and name)', async () => {
    const files = [
      { path: 'src/components/Button.tsx', name: 'Button.tsx', content: 'export const Button = () => null' },
      { path: 'src/utils/helper.ts', name: 'helper.ts', content: 'export const helper = () => {}' },
    ]

    const result = await zai.patch(files, 'add a comment at the top of each file').result()

    expect(result.output).toHaveLength(2)
    expect(result.output.find((f) => f.path === 'src/components/Button.tsx')).toBeDefined()
    expect(result.output.find((f) => f.path === 'src/utils/helper.ts')).toBeDefined()
    expect(result.output.find((f) => f.name === 'Button.tsx')).toBeDefined()
    expect(result.output.find((f) => f.name === 'helper.ts')).toBeDefined()

    expect(result.output.find((f) => f.name === 'Button.tsx')?.patch).toMatchInlineSnapshot(`"◼︎<001|//"`)
    expect(result.output.find((f) => f.name === 'helper.ts')?.patch).toMatchInlineSnapshot(`"◼︎<001|//"`)
  })

  it('handles file with special characters in content', async () => {
    const specialFile: File = {
      path: 'test.md',
      name: 'test.md',
      content: `# Title\n\nContent with \`code\` and **bold** and [links](url)`,
    }

    const result = await zai.patch([specialFile], 'add a new section called "## Installation"').result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].content).toContain('## Installation')
    expect(result.output[0].content).toContain('# Title')

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "# Title

      Content with \`code\` and **bold** and [links](url)
      ## Installation"
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎>3|## Installation"`)
  })

  it('patches a markdown table by adding a row', async () => {
    const tableFile: File = {
      path: 'fruits.md',
      name: 'fruits.md',
      content: `# Fruits

| Name   | Color  | Price |
| ------ | ------ | ----- |
| Apple  | Red    | 1.00  |
| Banana | Yellow | 0.50  |`,
    }

    const result = await zai
      .patch([tableFile], 'add a new row to the table for "Grape" with color "Purple" and price "2.00"')
      .result()

    expect(result.output).toHaveLength(1)
    const patched = result.output[0].content

    // Grape row exists and is well-formed (no doubled pipes like "||")
    expect(patched).toContain('Grape')
    expect(patched).toContain('Purple')
    expect(patched).toContain('2.00')
    expect(patched).not.toMatch(/\|\|/)

    // Existing rows are preserved
    expect(patched).toContain('| Apple  | Red    | 1.00  |')
    expect(patched).toContain('| Banana | Yellow | 0.50  |')

    // Every table row starts with a single pipe (not a double pipe)
    const tableRows = patched.split('\n').filter((l) => l.trim().startsWith('|'))
    for (const row of tableRows) {
      expect(row.trimStart().startsWith('||')).toBe(false)
    }
  })

  it('patches a markdown table by changing an existing row', async () => {
    const tableFile: File = {
      path: 'fruits.md',
      name: 'fruits.md',
      content: `# Fruits

| Name   | Color  | Price |
| ------ | ------ | ----- |
| Apple  | Red    | 1.00  |
| Banana | Yellow | 0.50  |`,
    }

    const result = await zai.patch([tableFile], 'change the price of Apple from 1.00 to 1.50').result()

    expect(result.output).toHaveLength(1)
    const patched = result.output[0].content

    expect(patched).toContain('1.50')
    expect(patched).not.toContain('| Apple  | Red    | 1.00  |')

    // No doubled pipes — guards against a bug where lines starting with `|`
    // get an extra leading pipe added by the patch protocol.
    expect(patched).not.toMatch(/\|\|/)

    // Banana row is untouched
    expect(patched).toContain('| Banana | Yellow | 0.50  |')

    // Every table row starts with a single pipe
    const tableRows = patched.split('\n').filter((l) => l.trim().startsWith('|'))
    for (const row of tableRows) {
      expect(row.trimStart().startsWith('||')).toBe(false)
    }
  })

  it('can add multiple sections to different files', async () => {
    const file1: File = { path: 'file1.txt', name: 'file1.txt', content: 'Line 1\nLine 2' }
    const file2: File = { path: 'file2.txt', name: 'file2.txt', content: 'Content A\nContent B' }

    const result = await zai.patch([file1, file2], 'add "# Header" at the beginning of each file').result()

    expect(result.output).toHaveLength(2)
    expect(result.output[0].content).toContain('# Header')
    expect(result.output[1].content).toContain('# Header')

    expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎<1|# Header"`)
    expect(result.output[1].patch).toMatchInlineSnapshot(`"◼︎<1|# Header"`)
  })

  it('replaces a range of lines', async () => {
    const multiLineFile: File = {
      path: 'example.ts',
      name: 'example.ts',
      content: `const a = 1
const b = 2
const c = 3
const d = 4
const e = 5`,
    }

    const result = await zai
      .patch(
        [multiLineFile],
        'replace lines 2 to 4 (const b through const d) with a single line: const sum = 2 + 3 + 4'
      )
      .result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].content).toContain('const sum')
    expect(result.output[0].content).toContain('const a = 1')
    expect(result.output[0].content).toContain('const e = 5')
    expect(result.output[0].content).not.toContain('const b = 2')
    expect(result.output[0].content).not.toContain('const c = 3')
    expect(result.output[0].content).not.toContain('const d = 4')

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "const a = 1
      const sum = 2 + 3 + 4
      const e = 5"
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎=2-4|const sum = 2 + 3 + 4"`)
  })

  it('ensures patched JSON is valid JSON', async () => {
    const jsonFile: File = {
      path: 'data.json',
      name: 'data.json',
      content: `{
  "users": [
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"}
  ],
  "count": 2
}`,
    }

    const result = await zai
      .patch([jsonFile], 'add a new user {"id": 3, "name": "Charlie"} to the users array and update count to 3')
      .result()

    expect(result.output).toHaveLength(1)
    // Verify it's valid JSON
    expect(() => JSON.parse(result.output[0].content)).not.toThrow()
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.users).toHaveLength(3)
    expect(parsed.count).toBe(3)
    expect(parsed.users[2].name).toBe('Charlie')

    expect(result.output[0].content).toMatchInlineSnapshot(`
      "{
        "users": [
          {"id": 1, "name": "Alice"},
          {"id": 2, "name": "Bob"},
          {"id": 3, "name": "Charlie"}
        ],
        "count": 3
      }"
    `)
    expect(result.output[0].patch).toMatchInlineSnapshot(`
      "◼︎=4|    {"id": 2, "name": "Bob"},
      ◼︎<5|    {"id": 3, "name": "Charlie"}
      ◼︎=6|  "count": 3"
    `)
  })

  it('handles large file with chunking', async () => {
    // Create a large file with many lines (but not TOO large)
    const largeContent = Array.from({ length: 1000 }, (_, i) => `const var${i} = ${i}`).join('\n')
    const largeFile: File = {
      path: 'large.ts',
      name: 'large.ts',
      content: largeContent,
    }

    const result = await zai.patch([largeFile], 'add a comment "// Variables" at the very top').result()

    expect(result.output).toHaveLength(1)
    expect(result.output[0].content).toContain('// Variables')
    expect(result.output[0].content).toContain('const var0 = 0')

    expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎<1|// Variables"`)
  })

  it('patches code that requires understanding context from two files', async () => {
    const apiFile: File = {
      path: 'src/api.ts',
      name: 'api.ts',
      content: `export interface User {
  id: number
  name: string
}

export function getUser(id: number): User {
  return { id, name: 'Unknown' }
}`,
    }

    const componentFile: File = {
      path: 'src/UserComponent.tsx',
      name: 'UserComponent.tsx',
      content: `import { getUser } from './api'

export function UserComponent({ userId }: { userId: number }) {
  const user = getUser(userId)
  return <div>{user.name}</div>
}`,
    }

    const result = await zai
      .patch(
        [apiFile, componentFile],
        'add an email field to the User interface and update the component to display it'
      )
      .result()

    expect(result.output).toHaveLength(2)
    const patchedApi = result.output.find((f) => f.name === 'api.ts')
    const patchedComponent = result.output.find((f) => f.name === 'UserComponent.tsx')

    // API should have email field
    expect(patchedApi?.content).toContain('email')
    // Component should display email
    expect(patchedComponent?.content).toContain('email')

    expect(patchedApi?.content).toMatchInlineSnapshot(`
      "export interface User {
        id: number
        name: string
        email: string
      }

      export function getUser(id: number): User {
        return { id, name: 'Unknown', email: 'unknown@example.com' }
      }"
    `)
    expect(patchedApi?.patch).toMatchInlineSnapshot(`
      "◼︎>003|  email: string
      ◼︎=007|  return { id, name: 'Unknown', email: 'unknown@example.com' }"
    `)
    expect(patchedComponent?.content).toMatchInlineSnapshot(`
      "import { getUser } from './api'

      export function UserComponent({ userId }: { userId: number }) {
        const user = getUser(userId)
        return <div>{user.name} ({user.email})</div>
      }"
    `)
    expect(patchedComponent?.patch).toMatchInlineSnapshot(`"◼︎=005|  return <div>{user.name} ({user.email})</div>"`)
  })

  it('refactors two files in a related way (rename function and update usage)', async () => {
    const utilsFile: File = {
      path: 'src/utils.ts',
      name: 'utils.ts',
      content: `export function calculateTotal(items: number[]): number {
  return items.reduce((sum, item) => sum + item, 0)
}`,
    }

    const appFile: File = {
      path: 'src/app.ts',
      name: 'app.ts',
      content: `import { calculateTotal } from './utils'

const prices = [10, 20, 30]
const total = calculateTotal(prices)
console.log(total)`,
    }

    const result = await zai
      .patch([utilsFile, appFile], 'rename the calculateTotal function to computeSum and update all usages')
      .result()

    expect(result.output).toHaveLength(2)
    const patchedUtils = result.output.find((f) => f.name === 'utils.ts')
    const patchedApp = result.output.find((f) => f.name === 'app.ts')

    // Utils should have new name
    expect(patchedUtils?.content).toContain('computeSum')
    expect(patchedUtils?.content).not.toContain('calculateTotal')
    // App should import and use new name
    expect(patchedApp?.content).toContain('computeSum')
    expect(patchedApp?.content).not.toContain('calculateTotal')

    expect(patchedUtils?.content).toMatchInlineSnapshot(`
      "export function computeSum(items: number[]): number {
        return items.reduce((sum, item) => sum + item, 0)
      }"
    `)
    expect(patchedUtils?.patch).toMatchInlineSnapshot(`"◼︎=001|export function computeSum(items: number[]): number {"`)
    expect(patchedApp?.content).toMatchInlineSnapshot(`
      "import { computeSum } from './utils'

      const prices = [10, 20, 30]
      const total = computeSum(prices)
      console.log(total)"
    `)
    expect(patchedApp?.patch).toMatchInlineSnapshot(`
      "◼︎=001|import { computeSum } from './utils'
      ◼︎=004|const total = computeSum(prices)"
    `)
  })

  it('leaves files unchanged when they do not need changes (5 files, only 2 need changes)', async () => {
    const asyncFile1: File = {
      path: 'src/api1.ts',
      name: 'api1.ts',
      content: `export async function fetchUser(id: number) {
  const response = await fetch('/api/user/' + id)
  return response.json()
}`,
    }

    const asyncFile2: File = {
      path: 'src/api2.ts',
      name: 'api2.ts',
      content: `export async function saveData(data: any) {
  const response = await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) })
  return response.json()
}`,
    }

    const syncFile1: File = {
      path: 'src/utils.ts',
      name: 'utils.ts',
      content: `export function formatName(first: string, last: string) {
  return first + ' ' + last
}`,
    }

    const syncFile2: File = {
      path: 'src/constants.ts',
      name: 'constants.ts',
      content: `export const API_URL = 'https://api.example.com'
export const TIMEOUT = 5000`,
    }

    const configFile: File = {
      path: 'config.json',
      name: 'config.json',
      content: `{
  "name": "app",
  "version": "1.0.0"
}`,
    }

    const files = [asyncFile1, asyncFile2, syncFile1, syncFile2, configFile]
    const result = await zai
      .patch(files, 'add try-catch error handling to all async functions that call fetch')
      .result()

    expect(result.output).toHaveLength(5)

    // Find the patched files
    const patchedApi1 = result.output.find((f) => f.name === 'api1.ts')
    const patchedApi2 = result.output.find((f) => f.name === 'api2.ts')
    const patchedUtils = result.output.find((f) => f.name === 'utils.ts')
    const patchedConstants = result.output.find((f) => f.name === 'constants.ts')
    const patchedConfig = result.output.find((f) => f.name === 'config.json')

    // Async files should have try-catch
    expect(patchedApi1?.content).toContain('try')
    expect(patchedApi1?.content).toContain('catch')
    expect(patchedApi2?.content).toContain('try')
    expect(patchedApi2?.content).toContain('catch')

    // Sync files and config should be unchanged
    expect(patchedUtils?.patch).toBe('')
    expect(patchedConstants?.patch).toBe('')
    expect(patchedConfig?.patch).toBe('')

    // Check that async files have patches
    expect(patchedApi1?.patch).not.toBe('')
    expect(patchedApi2?.patch).not.toBe('')

    expect(patchedApi1?.content).toMatchInlineSnapshot(`
      "export async function fetchUser(id: number) {
      try {
        const response = await fetch('/api/user/' + id)
        return response.json()
      } catch (error) {
        console.error('Error fetching user:', error)
        throw error
      }
      }"
    `)
    expect(patchedApi1?.patch).toMatchInlineSnapshot(`
      "◼︎=2-3|try {
        const response = await fetch('/api/user/' + id)
        return response.json()
      } catch (error) {
        console.error('Error fetching user:', error)
        throw error
      }"
    `)
    expect(patchedApi2?.content).toMatchInlineSnapshot(`
      "export async function saveData(data: any) {
      try {
        const response = await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) })
        return response.json()
      } catch (error) {
        console.error('Error saving data:', error)
        throw error
      }
      }"
    `)
    expect(patchedApi2?.patch).toMatchInlineSnapshot(`
      "◼︎=2-3|try {
        const response = await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) })
        return response.json()
      } catch (error) {
        console.error('Error saving data:', error)
        throw error
      }"
    `)
  })

  it('patches a file with many lines (500 lines) updating ~50 lines spread throughout', async () => {
    // Create a file with 500 lines where every 10th line has a TODO comment
    const lines: string[] = []
    for (let i = 0; i < 500; i++) {
      if (i % 10 === 0) {
        lines.push(`  // TODO: implement feature ${i / 10}`)
      } else {
        lines.push(`  const var${i} = ${i}`)
      }
    }

    const largeFile: File = {
      path: 'src/large.ts',
      name: 'large.ts',
      content: `function largeFunction() {\n${lines.join('\n')}\n}`,
    }

    const result = await zai.patch([largeFile], 'replace all TODO comments with FIXME comments').result()

    expect(result.output).toHaveLength(1)
    const patched = result.output[0]

    // All TODO should be replaced with FIXME
    expect(patched.content).not.toContain('TODO')
    expect(patched.content).toContain('FIXME')

    // Should still have the function structure
    expect(patched.content).toContain('function largeFunction()')
    expect(patched.content).toContain('const var1 = 1')
    expect(patched.content).toContain('const var499 = 499')

    // Count FIXME occurrences (should be 50)
    const fixmeCount = (patched.content.match(/FIXME/g) || []).length
    expect(fixmeCount).toBe(50)

    // Patch should have replacements
    expect(patched.patch).toContain('FIXME')
    expect(patched.patch).not.toBe('')

    expect(patched.content).toMatchInlineSnapshot(`
      "function largeFunction() {
        // FIXME: implement feature 0
        const var1 = 1
        const var2 = 2
        const var3 = 3
        const var4 = 4
        const var5 = 5
        const var6 = 6
        const var7 = 7
        const var8 = 8
        const var9 = 9
        // FIXME: implement feature 1
        const var11 = 11
        const var12 = 12
        const var13 = 13
        const var14 = 14
        const var15 = 15
        const var16 = 16
        const var17 = 17
        const var18 = 18
        const var19 = 19
        // FIXME: implement feature 2
        const var21 = 21
        const var22 = 22
        const var23 = 23
        const var24 = 24
        const var25 = 25
        const var26 = 26
        const var27 = 27
        const var28 = 28
        const var29 = 29
        // FIXME: implement feature 3
        const var31 = 31
        const var32 = 32
        const var33 = 33
        const var34 = 34
        const var35 = 35
        const var36 = 36
        const var37 = 37
        const var38 = 38
        const var39 = 39
        // FIXME: implement feature 4
        const var41 = 41
        const var42 = 42
        const var43 = 43
        const var44 = 44
        const var45 = 45
        const var46 = 46
        const var47 = 47
        const var48 = 48
        const var49 = 49
        // FIXME: implement feature 5
        const var51 = 51
        const var52 = 52
        const var53 = 53
        const var54 = 54
        const var55 = 55
        const var56 = 56
        const var57 = 57
        const var58 = 58
        const var59 = 59
        // FIXME: implement feature 6
        const var61 = 61
        const var62 = 62
        const var63 = 63
        const var64 = 64
        const var65 = 65
        const var66 = 66
        const var67 = 67
        const var68 = 68
        const var69 = 69
        // FIXME: implement feature 7
        const var71 = 71
        const var72 = 72
        const var73 = 73
        const var74 = 74
        const var75 = 75
        const var76 = 76
        const var77 = 77
        const var78 = 78
        const var79 = 79
        // FIXME: implement feature 8
        const var81 = 81
        const var82 = 82
        const var83 = 83
        const var84 = 84
        const var85 = 85
        const var86 = 86
        const var87 = 87
        const var88 = 88
        const var89 = 89
        // FIXME: implement feature 9
        const var91 = 91
        const var92 = 92
        const var93 = 93
        const var94 = 94
        const var95 = 95
        const var96 = 96
        const var97 = 97
        const var98 = 98
        const var99 = 99
        // FIXME: implement feature 10
        const var101 = 101
        const var102 = 102
        const var103 = 103
        const var104 = 104
        const var105 = 105
        const var106 = 106
        const var107 = 107
        const var108 = 108
        const var109 = 109
        // FIXME: implement feature 11
        const var111 = 111
        const var112 = 112
        const var113 = 113
        const var114 = 114
        const var115 = 115
        const var116 = 116
        const var117 = 117
        const var118 = 118
        const var119 = 119
        // FIXME: implement feature 12
        const var121 = 121
        const var122 = 122
        const var123 = 123
        const var124 = 124
        const var125 = 125
        const var126 = 126
        const var127 = 127
        const var128 = 128
        const var129 = 129
        // FIXME: implement feature 13
        const var131 = 131
        const var132 = 132
        const var133 = 133
        const var134 = 134
        const var135 = 135
        const var136 = 136
        const var137 = 137
        const var138 = 138
        const var139 = 139
        // FIXME: implement feature 14
        const var141 = 141
        const var142 = 142
        const var143 = 143
        const var144 = 144
        const var145 = 145
        const var146 = 146
        const var147 = 147
        const var148 = 148
        const var149 = 149
        // FIXME: implement feature 15
        const var151 = 151
        const var152 = 152
        const var153 = 153
        const var154 = 154
        const var155 = 155
        const var156 = 156
        const var157 = 157
        const var158 = 158
        const var159 = 159
        // FIXME: implement feature 16
        const var161 = 161
        const var162 = 162
        const var163 = 163
        const var164 = 164
        const var165 = 165
        const var166 = 166
        const var167 = 167
        const var168 = 168
        const var169 = 169
        // FIXME: implement feature 17
        const var171 = 171
        const var172 = 172
        const var173 = 173
        const var174 = 174
        const var175 = 175
        const var176 = 176
        const var177 = 177
        const var178 = 178
        const var179 = 179
        // FIXME: implement feature 18
        const var181 = 181
        const var182 = 182
        const var183 = 183
        const var184 = 184
        const var185 = 185
        const var186 = 186
        const var187 = 187
        const var188 = 188
        const var189 = 189
        // FIXME: implement feature 19
        const var191 = 191
        const var192 = 192
        const var193 = 193
        const var194 = 194
        const var195 = 195
        const var196 = 196
        const var197 = 197
        const var198 = 198
        const var199 = 199
        // FIXME: implement feature 20
        const var201 = 201
        const var202 = 202
        const var203 = 203
        const var204 = 204
        const var205 = 205
        const var206 = 206
        const var207 = 207
        const var208 = 208
        const var209 = 209
        // FIXME: implement feature 21
        const var211 = 211
        const var212 = 212
        const var213 = 213
        const var214 = 214
        const var215 = 215
        const var216 = 216
        const var217 = 217
        const var218 = 218
        const var219 = 219
        // FIXME: implement feature 22
        const var221 = 221
        const var222 = 222
        const var223 = 223
        const var224 = 224
        const var225 = 225
        const var226 = 226
        const var227 = 227
        const var228 = 228
        const var229 = 229
        // FIXME: implement feature 23
        const var231 = 231
        const var232 = 232
        const var233 = 233
        const var234 = 234
        const var235 = 235
        const var236 = 236
        const var237 = 237
        const var238 = 238
        const var239 = 239
        // FIXME: implement feature 24
        const var241 = 241
        const var242 = 242
        const var243 = 243
        const var244 = 244
        const var245 = 245
        const var246 = 246
        const var247 = 247
        const var248 = 248
        const var249 = 249
        // FIXME: implement feature 25
        const var251 = 251
        const var252 = 252
        const var253 = 253
        const var254 = 254
        const var255 = 255
        const var256 = 256
        const var257 = 257
        const var258 = 258
        const var259 = 259
        // FIXME: implement feature 26
        const var261 = 261
        const var262 = 262
        const var263 = 263
        const var264 = 264
        const var265 = 265
        const var266 = 266
        const var267 = 267
        const var268 = 268
        const var269 = 269
        // FIXME: implement feature 27
        const var271 = 271
        const var272 = 272
        const var273 = 273
        const var274 = 274
        const var275 = 275
        const var276 = 276
        const var277 = 277
        const var278 = 278
        const var279 = 279
        // FIXME: implement feature 28
        const var281 = 281
        const var282 = 282
        const var283 = 283
        const var284 = 284
        const var285 = 285
        const var286 = 286
        const var287 = 287
        const var288 = 288
        const var289 = 289
        // FIXME: implement feature 29
        const var291 = 291
        const var292 = 292
        const var293 = 293
        const var294 = 294
        const var295 = 295
        const var296 = 296
        const var297 = 297
        const var298 = 298
        const var299 = 299
        // FIXME: implement feature 30
        const var301 = 301
        const var302 = 302
        const var303 = 303
        const var304 = 304
        const var305 = 305
        const var306 = 306
        const var307 = 307
        const var308 = 308
        const var309 = 309
        // FIXME: implement feature 31
        const var311 = 311
        const var312 = 312
        const var313 = 313
        const var314 = 314
        const var315 = 315
        const var316 = 316
        const var317 = 317
        const var318 = 318
        const var319 = 319
        // FIXME: implement feature 32
        const var321 = 321
        const var322 = 322
        const var323 = 323
        const var324 = 324
        const var325 = 325
        const var326 = 326
        const var327 = 327
        const var328 = 328
        const var329 = 329
        // FIXME: implement feature 33
        const var331 = 331
        const var332 = 332
        const var333 = 333
        const var334 = 334
        const var335 = 335
        const var336 = 336
        const var337 = 337
        const var338 = 338
        const var339 = 339
        // FIXME: implement feature 34
        const var341 = 341
        const var342 = 342
        const var343 = 343
        const var344 = 344
        const var345 = 345
        const var346 = 346
        const var347 = 347
        const var348 = 348
        const var349 = 349
        // FIXME: implement feature 35
        const var351 = 351
        const var352 = 352
        const var353 = 353
        const var354 = 354
        const var355 = 355
        const var356 = 356
        const var357 = 357
        const var358 = 358
        const var359 = 359
        // FIXME: implement feature 36
        const var361 = 361
        const var362 = 362
        const var363 = 363
        const var364 = 364
        const var365 = 365
        const var366 = 366
        const var367 = 367
        const var368 = 368
        const var369 = 369
        // FIXME: implement feature 37
        const var371 = 371
        const var372 = 372
        const var373 = 373
        const var374 = 374
        const var375 = 375
        const var376 = 376
        const var377 = 377
        const var378 = 378
        const var379 = 379
        // FIXME: implement feature 38
        const var381 = 381
        const var382 = 382
        const var383 = 383
        const var384 = 384
        const var385 = 385
        const var386 = 386
        const var387 = 387
        const var388 = 388
        const var389 = 389
        // FIXME: implement feature 39
        const var391 = 391
        const var392 = 392
        const var393 = 393
        const var394 = 394
        const var395 = 395
        const var396 = 396
        const var397 = 397
        const var398 = 398
        const var399 = 399
        // FIXME: implement feature 40
        const var401 = 401
        const var402 = 402
        const var403 = 403
        const var404 = 404
        const var405 = 405
        const var406 = 406
        const var407 = 407
        const var408 = 408
        const var409 = 409
        // FIXME: implement feature 41
        const var411 = 411
        const var412 = 412
        const var413 = 413
        const var414 = 414
        const var415 = 415
        const var416 = 416
        const var417 = 417
        const var418 = 418
        const var419 = 419
        // FIXME: implement feature 42
        const var421 = 421
        const var422 = 422
        const var423 = 423
        const var424 = 424
        const var425 = 425
        const var426 = 426
        const var427 = 427
        const var428 = 428
        const var429 = 429
        // FIXME: implement feature 43
        const var431 = 431
        const var432 = 432
        const var433 = 433
        const var434 = 434
        const var435 = 435
        const var436 = 436
        const var437 = 437
        const var438 = 438
        const var439 = 439
        // FIXME: implement feature 44
        const var441 = 441
        const var442 = 442
        const var443 = 443
        const var444 = 444
        const var445 = 445
        const var446 = 446
        const var447 = 447
        const var448 = 448
        const var449 = 449
        // FIXME: implement feature 45
        const var451 = 451
        const var452 = 452
        const var453 = 453
        const var454 = 454
        const var455 = 455
        const var456 = 456
        const var457 = 457
        const var458 = 458
        const var459 = 459
        // FIXME: implement feature 46
        const var461 = 461
        const var462 = 462
        const var463 = 463
        const var464 = 464
        const var465 = 465
        const var466 = 466
        const var467 = 467
        const var468 = 468
        const var469 = 469
        // FIXME: implement feature 47
        const var471 = 471
        const var472 = 472
        const var473 = 473
        const var474 = 474
        const var475 = 475
        const var476 = 476
        const var477 = 477
        const var478 = 478
        const var479 = 479
        // FIXME: implement feature 48
        const var481 = 481
        const var482 = 482
        const var483 = 483
        const var484 = 484
        const var485 = 485
        const var486 = 486
        const var487 = 487
        const var488 = 488
        const var489 = 489
        // FIXME: implement feature 49
        const var491 = 491
        const var492 = 492
        const var493 = 493
        const var494 = 494
        const var495 = 495
        const var496 = 496
        const var497 = 497
        const var498 = 498
        const var499 = 499
      }"
    `)
    expect(patched.patch).toMatchInlineSnapshot(`
      "◼︎=2|  // FIXME: implement feature 0
      ◼︎=12|  // FIXME: implement feature 1
      ◼︎=22|  // FIXME: implement feature 2
      ◼︎=32|  // FIXME: implement feature 3
      ◼︎=42|  // FIXME: implement feature 4
      ◼︎=52|  // FIXME: implement feature 5
      ◼︎=62|  // FIXME: implement feature 6
      ◼︎=72|  // FIXME: implement feature 7
      ◼︎=82|  // FIXME: implement feature 8
      ◼︎=92|  // FIXME: implement feature 9
      ◼︎=102|  // FIXME: implement feature 10
      ◼︎=112|  // FIXME: implement feature 11
      ◼︎=122|  // FIXME: implement feature 12
      ◼︎=132|  // FIXME: implement feature 13
      ◼︎=142|  // FIXME: implement feature 14
      ◼︎=152|  // FIXME: implement feature 15
      ◼︎=162|  // FIXME: implement feature 16
      ◼︎=172|  // FIXME: implement feature 17
      ◼︎=182|  // FIXME: implement feature 18
      ◼︎=192|  // FIXME: implement feature 19
      ◼︎=202|  // FIXME: implement feature 20
      ◼︎=212|  // FIXME: implement feature 21
      ◼︎=222|  // FIXME: implement feature 22
      ◼︎=232|  // FIXME: implement feature 23
      ◼︎=242|  // FIXME: implement feature 24
      ◼︎=252|  // FIXME: implement feature 25
      ◼︎=262|  // FIXME: implement feature 26
      ◼︎=272|  // FIXME: implement feature 27
      ◼︎=282|  // FIXME: implement feature 28
      ◼︎=292|  // FIXME: implement feature 29
      ◼︎=302|  // FIXME: implement feature 30
      ◼︎=312|  // FIXME: implement feature 31
      ◼︎=322|  // FIXME: implement feature 32
      ◼︎=332|  // FIXME: implement feature 33
      ◼︎=342|  // FIXME: implement feature 34
      ◼︎=352|  // FIXME: implement feature 35
      ◼︎=362|  // FIXME: implement feature 36
      ◼︎=372|  // FIXME: implement feature 37
      ◼︎=382|  // FIXME: implement feature 38
      ◼︎=392|  // FIXME: implement feature 39
      ◼︎=402|  // FIXME: implement feature 40
      ◼︎=412|  // FIXME: implement feature 41
      ◼︎=422|  // FIXME: implement feature 42
      ◼︎=432|  // FIXME: implement feature 43
      ◼︎=442|  // FIXME: implement feature 44
      ◼︎=452|  // FIXME: implement feature 45
      ◼︎=462|  // FIXME: implement feature 46
      ◼︎=472|  // FIXME: implement feature 47
      ◼︎=482|  // FIXME: implement feature 48
      ◼︎=492|  // FIXME: implement feature 49"
    `)
  })

  it('refactors a function in a large file (200 lines): renames it, changes parameters, updates all usages', async () => {
    // Create a 200-line file with a function defined at the top and used throughout
    const lines: string[] = []
    lines.push('// Utility functions')
    lines.push('function processItem(item: string): string {')
    lines.push('  return item.toUpperCase()')
    lines.push('}')
    lines.push('')
    lines.push('// Main code')

    // Add 194 more lines with multiple usages of processItem
    for (let i = 0; i < 194; i++) {
      if (i % 20 === 0) {
        lines.push(`const result${i} = processItem('value${i}')`)
      } else if (i % 30 === 0) {
        lines.push(`console.log(processItem('test${i}'))`)
      } else {
        lines.push(`const data${i} = 'some data ${i}'`)
      }
    }

    const largeFile: File = {
      path: 'src/processor.ts',
      name: 'processor.ts',
      content: lines.join('\n'),
    }

    const result = await zai
      .patch(
        [largeFile],
        'rename processItem to transformText and add a second parameter "options: { uppercase: boolean }" with default value { uppercase: false }. Update all function calls to use the new signature. For string where number value in the 1st param is above 100, make it uppercase.'
      )
      .result()

    expect(result.output).toHaveLength(1)
    const patched = result.output[0]

    // Should not have old function name
    expect(patched.content).not.toContain('processItem')

    // Should have new function name
    expect(patched.content).toContain('transformText')

    // Should have the new parameter
    expect(patched.content).toContain('options')
    expect(patched.content).toContain('uppercase')

    // All usages should be updated
    const transformTextCount = (patched.content.match(/transformText/g) || []).length
    expect(transformTextCount).toBeGreaterThan(1) // Function definition + usages

    // Patch should not be empty
    expect(patched.patch).not.toBe('')

    expect(patched.content).toMatchInlineSnapshot(`
      "// Utility functions
      function transformText(item: string, options: { uppercase: boolean } = { uppercase: false }): string {
        return options.uppercase ? item.toUpperCase() : item
      }

      // Main code
      const result0 = transformText('value0', { uppercase: false })
      const data1 = 'some data 1'
      const data2 = 'some data 2'
      const data3 = 'some data 3'
      const data4 = 'some data 4'
      const data5 = 'some data 5'
      const data6 = 'some data 6'
      const data7 = 'some data 7'
      const data8 = 'some data 8'
      const data9 = 'some data 9'
      const data10 = 'some data 10'
      const data11 = 'some data 11'
      const data12 = 'some data 12'
      const data13 = 'some data 13'
      const data14 = 'some data 14'
      const data15 = 'some data 15'
      const data16 = 'some data 16'
      const data17 = 'some data 17'
      const data18 = 'some data 18'
      const data19 = 'some data 19'
      const result20 = transformText('value20', { uppercase: false })
      const data21 = 'some data 21'
      const data22 = 'some data 22'
      const data23 = 'some data 23'
      const data24 = 'some data 24'
      const data25 = 'some data 25'
      const data26 = 'some data 26'
      const data27 = 'some data 27'
      const data28 = 'some data 28'
      const data29 = 'some data 29'
      console.log(transformText('test30', { uppercase: false }))
      const data31 = 'some data 31'
      const data32 = 'some data 32'
      const data33 = 'some data 33'
      const data34 = 'some data 34'
      const data35 = 'some data 35'
      const data36 = 'some data 36'
      const data37 = 'some data 37'
      const data38 = 'some data 38'
      const data39 = 'some data 39'
      const result40 = transformText('value40', { uppercase: false })
      const data41 = 'some data 41'
      const data42 = 'some data 42'
      const data43 = 'some data 43'
      const data44 = 'some data 44'
      const data45 = 'some data 45'
      const data46 = 'some data 46'
      const data47 = 'some data 47'
      const data48 = 'some data 48'
      const data49 = 'some data 49'
      const data50 = 'some data 50'
      const data51 = 'some data 51'
      const data52 = 'some data 52'
      const data53 = 'some data 53'
      const data54 = 'some data 54'
      const data55 = 'some data 55'
      const data56 = 'some data 56'
      const data57 = 'some data 57'
      const data58 = 'some data 58'
      const data59 = 'some data 59'
      const result60 = transformText('value60', { uppercase: false })
      const data61 = 'some data 61'
      const data62 = 'some data 62'
      const data63 = 'some data 63'
      const data64 = 'some data 64'
      const data65 = 'some data 65'
      const data66 = 'some data 66'
      const data67 = 'some data 67'
      const data68 = 'some data 68'
      const data69 = 'some data 69'
      const data70 = 'some data 70'
      const data71 = 'some data 71'
      const data72 = 'some data 72'
      const data73 = 'some data 73'
      const data74 = 'some data 74'
      const data75 = 'some data 75'
      const data76 = 'some data 76'
      const data77 = 'some data 77'
      const data78 = 'some data 78'
      const data79 = 'some data 79'
      const result80 = transformText('value80', { uppercase: false })
      const data81 = 'some data 81'
      const data82 = 'some data 82'
      const data83 = 'some data 83'
      const data84 = 'some data 84'
      const data85 = 'some data 85'
      const data86 = 'some data 86'
      const data87 = 'some data 87'
      const data88 = 'some data 88'
      const data89 = 'some data 89'
      console.log(transformText('test90', { uppercase: false }))
      const data91 = 'some data 91'
      const data92 = 'some data 92'
      const data93 = 'some data 93'
      const data94 = 'some data 94'
      const data95 = 'some data 95'
      const data96 = 'some data 96'
      const data97 = 'some data 97'
      const data98 = 'some data 98'
      const data99 = 'some data 99'
      const result100 = transformText('value100', { uppercase: false })
      const data101 = 'some data 101'
      const data102 = 'some data 102'
      const data103 = 'some data 103'
      const data104 = 'some data 104'
      const data105 = 'some data 105'
      const data106 = 'some data 106'
      const data107 = 'some data 107'
      const data108 = 'some data 108'
      const data109 = 'some data 109'
      const data110 = 'some data 110'
      const data111 = 'some data 111'
      const data112 = 'some data 112'
      const data113 = 'some data 113'
      const data114 = 'some data 114'
      const data115 = 'some data 115'
      const data116 = 'some data 116'
      const data117 = 'some data 117'
      const data118 = 'some data 118'
      const data119 = 'some data 119'
      const result120 = transformText('value120', { uppercase: true })
      const data121 = 'some data 121'
      const data122 = 'some data 122'
      const data123 = 'some data 123'
      const data124 = 'some data 124'
      const data125 = 'some data 125'
      const data126 = 'some data 126'
      const data127 = 'some data 127'
      const data128 = 'some data 128'
      const data129 = 'some data 129'
      const data130 = 'some data 130'
      const data131 = 'some data 131'
      const data132 = 'some data 132'
      const data133 = 'some data 133'
      const data134 = 'some data 134'
      const data135 = 'some data 135'
      const data136 = 'some data 136'
      const data137 = 'some data 137'
      const data138 = 'some data 138'
      const data139 = 'some data 139'
      const result140 = transformText('value140', { uppercase: true })
      const data141 = 'some data 141'
      const data142 = 'some data 142'
      const data143 = 'some data 143'
      const data144 = 'some data 144'
      const data145 = 'some data 145'
      const data146 = 'some data 146'
      const data147 = 'some data 147'
      const data148 = 'some data 148'
      const data149 = 'some data 149'
      console.log(transformText('test150', { uppercase: true }))
      const data151 = 'some data 151'
      const data152 = 'some data 152'
      const data153 = 'some data 153'
      const data154 = 'some data 154'
      const data155 = 'some data 155'
      const data156 = 'some data 156'
      const data157 = 'some data 157'
      const data158 = 'some data 158'
      const data159 = 'some data 159'
      const result160 = transformText('value160', { uppercase: true })
      const data161 = 'some data 161'
      const data162 = 'some data 162'
      const data163 = 'some data 163'
      const data164 = 'some data 164'
      const data165 = 'some data 165'
      const data166 = 'some data 166'
      const data167 = 'some data 167'
      const data168 = 'some data 168'
      const data169 = 'some data 169'
      const data170 = 'some data 170'
      const data171 = 'some data 171'
      const data172 = 'some data 172'
      const data173 = 'some data 173'
      const data174 = 'some data 174'
      const data175 = 'some data 175'
      const data176 = 'some data 176'
      const data177 = 'some data 177'
      const data178 = 'some data 178'
      const data179 = 'some data 179'
      const result180 = transformText('value180', { uppercase: true })
      const data181 = 'some data 181'
      const data182 = 'some data 182'
      const data183 = 'some data 183'
      const data184 = 'some data 184'
      const data185 = 'some data 185'
      const data186 = 'some data 186'
      const data187 = 'some data 187'
      const data188 = 'some data 188'
      const data189 = 'some data 189'
      const data190 = 'some data 190'
      const data191 = 'some data 191'
      const data192 = 'some data 192'
      const data193 = 'some data 193'"
    `)
    expect(patched.patch).toMatchInlineSnapshot(`
      "◼︎=2-4|function transformText(item: string, options: { uppercase: boolean } = { uppercase: false }): string {
        return options.uppercase ? item.toUpperCase() : item
      }
      ◼︎=7|const result0 = transformText('value0', { uppercase: false })
      ◼︎=27|const result20 = transformText('value20', { uppercase: false })
      ◼︎=37|console.log(transformText('test30', { uppercase: false }))
      ◼︎=47|const result40 = transformText('value40', { uppercase: false })
      ◼︎=67|const result60 = transformText('value60', { uppercase: false })
      ◼︎=87|const result80 = transformText('value80', { uppercase: false })
      ◼︎=97|console.log(transformText('test90', { uppercase: false }))
      ◼︎=107|const result100 = transformText('value100', { uppercase: false })
      ◼︎=127|const result120 = transformText('value120', { uppercase: true })
      ◼︎=147|const result140 = transformText('value140', { uppercase: true })
      ◼︎=157|console.log(transformText('test150', { uppercase: true }))
      ◼︎=167|const result160 = transformText('value160', { uppercase: true })
      ◼︎=187|const result180 = transformText('value180', { uppercase: true })"
    `)
  })

  it('handles large file with chunking enabled (single file > maxTokensPerChunk)', async () => {
    // Create a very large file (2000 lines)
    const lines: string[] = []
    for (let i = 0; i < 2000; i++) {
      if (i % 100 === 0) {
        lines.push(`  // TODO: section ${i / 100}`)
      } else {
        lines.push(`  const variable${i} = ${i}`)
      }
    }

    const veryLargeFile: File = {
      path: 'src/giant.ts',
      name: 'giant.ts',
      content: `function giant() {\n${lines.join('\n')}\n}`,
    }

    const result = await zai
      .patch([veryLargeFile], 'replace all TODO comments with DONE comments', {
        maxTokensPerChunk: 5000, // Force chunking
      })
      .result()

    expect(result.output).toHaveLength(1)
    const patched = result.output[0]

    // Verify chunking occurred - multiple LLM calls were made
    expect(result.usage.requests.requests).toBeGreaterThan(1)
    console.log(`Chunking test: ${result.usage.requests.requests} LLM calls made`)

    // All TODO should be replaced with DONE
    expect(patched.content).not.toContain('TODO')
    expect(patched.content).toContain('DONE')

    // Should still have the function structure
    expect(patched.content).toContain('function giant()')

    // Count DONE occurrences
    const doneCount = (patched.content.match(/DONE/g) || []).length
    expect(doneCount).toBe(20)

    expect(patched.patch).toContain('DONE')
    expect(patched.patch).not.toBe('')
  })

  it('handles multiple files with chunking (total files > maxTokensPerChunk)', async () => {
    // Create 5 moderately sized files
    const files: File[] = []
    for (let fileIdx = 0; fileIdx < 5; fileIdx++) {
      const lines: string[] = []
      for (let i = 0; i < 300; i++) {
        if (i % 50 === 0) {
          lines.push(`  // TODO: feature ${fileIdx}-${i / 50}`)
        } else {
          lines.push(`  const var${i} = ${i}`)
        }
      }

      files.push({
        path: `src/file${fileIdx}.ts`,
        name: `file${fileIdx}.ts`,
        content: `function file${fileIdx}() {\n${lines.join('\n')}\n}`,
      })
    }

    const result = await zai
      .patch(files, 'replace all TODO comments with COMPLETED comments', {
        maxTokensPerChunk: 8000, // Force batching
      })
      .result()

    expect(result.output).toHaveLength(5)

    // Verify batching occurred - multiple LLM calls were made
    expect(result.usage.requests.requests).toBeGreaterThan(1)
    console.log(`Batching test: ${result.usage.requests.requests} LLM calls made`)

    // Check that all files were processed
    for (let i = 0; i < 5; i++) {
      const patched = result.output.find((f) => f.name === `file${i}.ts`)
      expect(patched).toBeDefined()
      expect(patched?.content).not.toContain('TODO')
      expect(patched?.content).toContain('COMPLETED')

      // Each file should have 6 COMPLETED comments
      const completedCount = (patched?.content.match(/COMPLETED/g) || []).length
      expect(completedCount).toBe(6)
    }
  })
})

describe.sequential('zai.learn.patch', { timeout: 60_000 }, () => {
  const client = getClient()
  let tableName = 'ZaiTestPatchInternalTable'
  let taskId = 'patch'
  let zai = getZai()

  beforeEach(async () => {
    zai = getZai().with({
      activeLearning: {
        enable: true,
        taskId,
        tableName,
      },
    })
  })

  afterEach(async () => {
    try {
      await client.deleteTableRows({ table: tableName, deleteAllRows: true })
    } catch (err) {}
  })

  afterAll(async () => {
    try {
      await client.deleteTable({ table: tableName })
    } catch (err) {}
  })

  it('learns patch patterns from examples', async () => {
    const adapter = new TableAdapter({
      client,
      tableName,
    })

    const testFile: File = {
      path: 'component.tsx',
      name: 'component.tsx',
      content: 'export const Component = () => <div>Test</div>',
    }

    // First call - learns the pattern
    const first = await zai.learn(taskId).patch([testFile], 'add TypeScript FC type').result()

    let rows = await client.findTableRows({ table: tableName })
    expect(rows.rows.length).toBeGreaterThanOrEqual(1)

    // Save an approved example with a specific pattern
    await adapter.saveExample({
      key: 'example1',
      taskId: `zai/${taskId}`,
      taskType: 'zai.patch',
      instructions: 'add TypeScript FC type',
      input: [
        {
          path: 'test.tsx',
          name: 'test.tsx',
          content: 'export const MyComponent = () => <div>Hello</div>',
        },
      ],
      output: [
        {
          path: 'test.tsx',
          name: 'test.tsx',
          content: 'import { FC } from "react"\n\nexport const MyComponent: FC = () => <div>Hello</div>',
        },
      ],
      metadata,
      status: 'approved',
    })

    // Second call - should use the learned pattern
    const second = await zai.learn(taskId).patch([testFile], 'add TypeScript FC type').result()

    rows = await client.findTableRows({ table: tableName })
    expect(rows.rows.length).toBe(2)

    expect(second.output[0].content).toContain('FC')
    expect(second.output[0].content).toContain('react')

    expect(second.output[0].patch).toMatchInlineSnapshot(`
      "◼︎<1|import React from 'react';
      ◼︎=1|export const Component: React.FC = () => <div>Test</div>"
    `)
  })
})

describe('zai.patch — JSON file operations', { timeout: 60_000 }, () => {
  const zai = getZai()

  it('changes a value inside a simple JSON object', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify({ name: 'my-app', version: '1.0.0', debug: false }, null, 2),
    }

    const { output } = await zai.patch([file], 'change the version to "2.0.0"').result()

    expect(output).toHaveLength(1)
    expect(output[0].name).toBe('config.json')
    expect(output[0].path).toBe('config.json')
    expect(output[0].content).toMatch(JSON.stringify({ name: 'my-app', version: '2.0.0', debug: false }, null, 2))
  })

  it('changes multiple properties inside a simple JSON object', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify({ name: 'my-app', version: '1.0.0', debug: false, port: 3000 }, null, 2),
    }

    const result = await zai
      .patch([file], 'make the name "super-app", debug = true, change port should be 8080')
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.name).toBe('super-app')
    expect(parsed.debug).toBe(true)
    expect(parsed.port).toBe(8080)
    expect(parsed.version).toBe('1.0.0')
  })

  it('changes multiple properties inside a complex JSON object', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify(
        {
          app: { name: 'my-app', version: '1.0.0' },
          server: { host: 'localhost', port: 3000, ssl: { enabled: false, cert: '' } },
          logging: { level: 'info', format: 'text' },
        },
        null,
        2
      ),
    }

    const result = await zai
      .patch(
        [file],
        'change app.name to "production-app", server.port to 443, server.ssl.enabled to true, and logging.level to "warn"'
      )
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.app.name).toBe('production-app')
    expect(parsed.app.version).toBe('1.0.0')
    expect(parsed.server.port).toBe(443)
    expect(parsed.server.ssl.enabled).toBe(true)
    expect(parsed.server.host).toBe('localhost')
    expect(parsed.logging.level).toBe('warn')
    expect(parsed.logging.format).toBe('text')
  })

  it('adds additional properties in a simple JSON', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify({ name: 'my-app', version: '1.0.0' }, null, 2),
    }

    const result = await zai
      .patch([file], 'add "description": "A sample application" and "license": "MIT" to the JSON')
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.name).toBe('my-app')
    expect(parsed.version).toBe('1.0.0')
    expect(parsed.description).toBe('A sample application')
    expect(parsed.license).toBe('MIT')
  })

  it('adds additional properties in a complex JSON', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify(
        {
          database: { host: 'localhost', port: 5432 },
          cache: { enabled: true },
        },
        null,
        2
      ),
    }

    const result = await zai
      .patch(
        [file],
        'add "name": "mydb" and "ssl": true inside the database object, and add "ttl": 3600 inside the cache object'
      )
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.database.host).toBe('localhost')
    expect(parsed.database.port).toBe(5432)
    expect(parsed.database.name).toBe('mydb')
    expect(parsed.database.ssl).toBe(true)
    expect(parsed.cache.enabled).toBe(true)
    expect(parsed.cache.ttl).toBe(3600)
  })

  it('removes a property from a simple JSON', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify({ name: 'my-app', version: '1.0.0', deprecated: true, debug: false }, null, 2),
    }

    const result = await zai.patch([file], 'remove the "deprecated" property').result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.name).toBe('my-app')
    expect(parsed.version).toBe('1.0.0')
    expect(parsed.debug).toBe(false)
    expect(parsed).not.toHaveProperty('deprecated')
  })

  it('removes multiple properties from a simple JSON', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify(
        { name: 'my-app', version: '1.0.0', deprecated: true, temporary: 'value', debug: false, legacy: true },
        null,
        2
      ),
    }

    const result = await zai.patch([file], 'remove the "deprecated", "temporary", and "legacy" properties').result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.name).toBe('my-app')
    expect(parsed.version).toBe('1.0.0')
    expect(parsed.debug).toBe(false)
    expect(parsed).not.toHaveProperty('deprecated')
    expect(parsed).not.toHaveProperty('temporary')
    expect(parsed).not.toHaveProperty('legacy')
  })

  it('removes items inside a JSON array inside a JSON object', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify(
        {
          name: 'my-app',
          contributors: [
            { name: 'Alice', role: 'lead' },
            { name: 'Bob', role: 'dev' },
            { name: 'Charlie', role: 'intern' },
            { name: 'Diana', role: 'dev' },
          ],
        },
        null,
        2
      ),
    }

    const result = await zai.patch([file], 'remove Bob and Charlie from the contributors array').result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.name).toBe('my-app')
    expect(parsed.contributors).toHaveLength(2)
    expect(parsed.contributors.map((c: any) => c.name)).toEqual(['Alice', 'Diana'])
  })

  it('adds, removes, and changes properties inside a simple JSON', async () => {
    const file: File = {
      path: 'package.json',
      name: 'package.json',
      content: JSON.stringify({ name: 'old-name', version: '0.1.0', private: true, deprecated: true }, null, 2),
    }

    const result = await zai
      .patch(
        [file],
        'change "name" to "new-name", change "version" to "1.0.0", remove the "deprecated" property, and add "license": "MIT"'
      )
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    expect(parsed.name).toBe('new-name')
    expect(parsed.version).toBe('1.0.0')
    expect(parsed.private).toBe(true)
    expect(parsed.license).toBe('MIT')
    expect(parsed).not.toHaveProperty('deprecated')
  })

  it('adds, removes, and changes properties inside a complex JSON', async () => {
    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify(
        {
          app: { name: 'old-app', version: '0.1.0', experimental: true },
          database: { host: 'localhost', port: 5432, pool: { min: 2, max: 10, idleTimeout: 30000 } },
          features: { darkMode: false, betaFeatures: true, analytics: { enabled: false, provider: 'none' } },
        },
        null,
        2
      ),
    }

    const result = await zai
      .patch(
        [file],
        'change app.name to "new-app" and app.version to "1.0.0", remove app.experimental, change database.pool.max to 20, remove features.betaFeatures, change features.analytics.enabled to true and features.analytics.provider to "mixpanel", and add features.darkMode as true and database.pool.timeout as 5000'
      )
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)

    // Changed
    expect(parsed.app.name).toBe('new-app')
    expect(parsed.app.version).toBe('1.0.0')
    expect(parsed.database.pool.max).toBe(20)
    expect(parsed.features.analytics.enabled).toBe(true)
    expect(parsed.features.analytics.provider).toBe('mixpanel')
    expect(parsed.features.darkMode).toBe(true)

    // Removed
    expect(parsed.app).not.toHaveProperty('experimental')
    expect(parsed.features).not.toHaveProperty('betaFeatures')

    // Added
    expect(parsed.database.pool.timeout).toBe(5000)

    // Unchanged
    expect(parsed.database.host).toBe('localhost')
    expect(parsed.database.port).toBe(5432)
    expect(parsed.database.pool.min).toBe(2)
    expect(parsed.database.pool.idleTimeout).toBe(30000)
  })

  it('restructures a realistic package.json with many simultaneous changes', async () => {
    const file: File = {
      path: 'package.json',
      name: 'package.json',
      content: JSON.stringify(
        {
          name: '@acme/api-server',
          version: '2.4.1',
          description: 'Acme API Server',
          main: 'dist/index.js',
          scripts: {
            build: 'tsc',
            dev: 'ts-node src/index.ts',
            test: 'jest',
            lint: 'eslint src/',
            'db:migrate': 'knex migrate:latest',
            'db:seed': 'knex seed:run',
          },
          dependencies: {
            express: '^4.18.2',
            knex: '^3.1.0',
            pg: '^8.11.3',
            zod: '^3.22.4',
            dotenv: '^16.3.1',
            cors: '^2.8.5',
            helmet: '^7.1.0',
            jsonwebtoken: '^9.0.2',
            bcrypt: '^5.1.1',
          },
          devDependencies: {
            typescript: '^5.3.3',
            '@types/node': '^20.10.0',
            '@types/express': '^4.17.21',
            jest: '^29.7.0',
            'ts-jest': '^29.1.1',
            eslint: '^8.55.0',
            '@types/cors': '^2.8.17',
            '@types/jsonwebtoken': '^9.0.5',
            '@types/bcrypt': '^5.0.2',
          },
          engines: { node: '>=18.0.0' },
          repository: {
            type: 'git',
            url: 'https://github.com/acme/api-server.git',
          },
          author: 'Acme Corp',
          license: 'UNLICENSED',
          private: true,
        },
        null,
        2
      ),
    }

    const result = await zai
      .patch(
        [file],
        [
          'Migrate from Express+Jest+ESLint to Hono+Vitest+Biome:',
          '- change scripts.build to "bun build src/index.ts --outdir dist"',
          '- change scripts.dev to "bun --hot src/index.ts"',
          '- change scripts.test to "vitest"',
          '- change scripts.lint to "biome check src/"',
          '- remove express, cors, helmet, dotenv from dependencies',
          '- add hono "^4.0.0" to dependencies',
          '- remove all @types/* packages, jest, ts-jest, eslint from devDependencies',
          '- add vitest "^1.2.0" and @biomejs/biome "^1.5.0" to devDependencies',
          '- bump the version to "3.0.0"',
          '- change engines.node to ">=20.0.0"',
          '- change license to "MIT"',
          '- remove the private field',
          '- add a "type": "module" field',
        ].join('\n')
      )
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)

    // Version & metadata
    expect(parsed.version).toBe('3.0.0')
    expect(parsed.license).toBe('MIT')
    expect(parsed).not.toHaveProperty('private')
    expect(parsed.type).toBe('module')
    expect(parsed.engines.node).toBe('>=20.0.0')

    // Scripts
    expect(parsed.scripts.test).toBe('vitest')
    expect(parsed.scripts.lint).toBe('biome check src/')
    expect(parsed.scripts.dev).toContain('bun')
    expect(parsed.scripts.build).toContain('bun')

    // Dependencies — removed
    expect(parsed.dependencies).not.toHaveProperty('express')
    expect(parsed.dependencies).not.toHaveProperty('cors')
    expect(parsed.dependencies).not.toHaveProperty('helmet')
    expect(parsed.dependencies).not.toHaveProperty('dotenv')

    // Dependencies — added
    expect(parsed.dependencies.hono).toBeDefined()

    // Dependencies — kept
    expect(parsed.dependencies.knex).toBeDefined()
    expect(parsed.dependencies.pg).toBeDefined()
    expect(parsed.dependencies.zod).toBeDefined()
    expect(parsed.dependencies.jsonwebtoken).toBeDefined()
    expect(parsed.dependencies.bcrypt).toBeDefined()

    // DevDependencies — removed
    expect(parsed.devDependencies).not.toHaveProperty('jest')
    expect(parsed.devDependencies).not.toHaveProperty('ts-jest')
    expect(parsed.devDependencies).not.toHaveProperty('eslint')
    expect(parsed.devDependencies).not.toHaveProperty('@types/node')
    expect(parsed.devDependencies).not.toHaveProperty('@types/express')
    expect(parsed.devDependencies).not.toHaveProperty('@types/cors')
    expect(parsed.devDependencies).not.toHaveProperty('@types/jsonwebtoken')
    expect(parsed.devDependencies).not.toHaveProperty('@types/bcrypt')

    // DevDependencies — added
    expect(parsed.devDependencies.vitest).toBeDefined()
    expect(parsed.devDependencies['@biomejs/biome']).toBeDefined()

    // DevDependencies — kept
    expect(parsed.devDependencies.typescript).toBeDefined()

    // Unchanged
    expect(parsed.name).toBe('@acme/api-server')
    expect(parsed.description).toBe('Acme API Server')
    expect(parsed.scripts['db:migrate']).toBe('knex migrate:latest')
    expect(parsed.scripts['db:seed']).toBe('knex seed:run')
    expect(parsed.repository.url).toBe('https://github.com/acme/api-server.git')
    expect(parsed.author).toBe('Acme Corp')
  })

  it('restructures deep nested objects and arrays in a CI/CD pipeline config', async () => {
    const file: File = {
      path: 'pipeline.json',
      name: 'pipeline.json',
      content: JSON.stringify(
        {
          pipeline: {
            name: 'deploy-prod',
            version: 1,
            triggers: [
              { type: 'push', branches: ['main', 'release/*'] },
              { type: 'schedule', cron: '0 2 * * 1' },
              { type: 'manual', approvers: ['alice', 'bob', 'charlie'] },
            ],
            stages: [
              {
                name: 'build',
                timeout: 600,
                steps: [
                  { run: 'npm ci', cache: true },
                  { run: 'npm run build', artifacts: ['dist/**'] },
                  { run: 'npm test', retry: 2 },
                ],
              },
              {
                name: 'security',
                timeout: 300,
                steps: [
                  { run: 'npm audit', allowFailure: true },
                  { run: 'snyk test', env: { SNYK_TOKEN: '$SNYK_TOKEN' } },
                ],
              },
              {
                name: 'deploy',
                timeout: 900,
                dependsOn: ['build', 'security'],
                steps: [
                  { run: 'docker build -t app:latest .' },
                  { run: 'docker push app:latest' },
                  { run: 'kubectl apply -f k8s/', retry: 3 },
                ],
              },
            ],
            notifications: {
              onSuccess: [{ type: 'slack', channel: '#deploys' }],
              onFailure: [
                { type: 'slack', channel: '#deploys' },
                { type: 'email', recipients: ['oncall@acme.com'] },
                { type: 'pagerduty', severity: 'high' },
              ],
            },
          },
        },
        null,
        2
      ),
    }

    const result = await zai
      .patch(
        [file],
        [
          'Make the following changes to the pipeline config:',
          '- bump pipeline.version to 2',
          '- remove the schedule trigger from the triggers array',
          '- remove "charlie" from the manual trigger approvers and add "diana"',
          '- remove the entire security stage from stages',
          '- in the deploy stage: remove "security" from dependsOn (keep "build"), and change retry on the kubectl step to 5',
          '- in notifications.onFailure: remove the pagerduty entry and add "platform@acme.com" to the email recipients',
        ].join('\n')
      )
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)
    const p = parsed.pipeline

    // Version
    expect(p.version).toBe(2)

    // Triggers — schedule removed
    expect(p.triggers.find((t: any) => t.type === 'schedule')).toBeUndefined()
    // Triggers — push unchanged
    const pushTrigger = p.triggers.find((t: any) => t.type === 'push')
    expect(pushTrigger.branches).toContain('main')
    expect(pushTrigger.branches).toContain('release/*')
    // Triggers — manual approvers updated
    const manualTrigger = p.triggers.find((t: any) => t.type === 'manual')
    expect(manualTrigger.approvers).toContain('alice')
    expect(manualTrigger.approvers).toContain('bob')
    expect(manualTrigger.approvers).not.toContain('charlie')
    expect(manualTrigger.approvers).toContain('diana')

    // Stages — security removed
    expect(p.stages.find((s: any) => s.name === 'security')).toBeUndefined()

    // Stages — build unchanged
    const build = p.stages.find((s: any) => s.name === 'build')
    expect(build.timeout).toBe(600)
    expect(build.steps).toHaveLength(3)

    // Stages — deploy
    const deploy = p.stages.find((s: any) => s.name === 'deploy')
    expect(deploy.dependsOn).toContain('build')
    expect(deploy.dependsOn).not.toContain('security')
    const kubectlStep = deploy.steps.find((s: any) => s.run.includes('kubectl'))
    expect(kubectlStep.retry).toBe(5)

    // Notifications — pagerduty removed
    expect(p.notifications.onFailure.find((n: any) => n.type === 'pagerduty')).toBeUndefined()
    // Notifications — email recipients updated
    const emailNotif = p.notifications.onFailure.find((n: any) => n.type === 'email')
    expect(emailNotif.recipients).toContain('oncall@acme.com')
    expect(emailNotif.recipients).toContain('platform@acme.com')

    // Unchanged
    expect(p.name).toBe('deploy-prod')
    expect(p.notifications.onSuccess).toHaveLength(1)
    expect(p.notifications.onSuccess[0].channel).toBe('#deploys')
  })

  it('validates patched JSON against a zui schema', async () => {
    const schema = z.object({
      name: z.string(),
      version: z.string(),
      port: z.number().min(1).max(65535),
      debug: z.boolean(),
    })

    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify({ name: 'my-app', version: '1.0.0', port: 3000, debug: false }, null, 2),
    }

    const result = await zai.patch([file], 'change port to 8080 and set debug to true', { schema }).result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)

    // Schema validation should pass
    const safe = schema.safeParse(parsed)
    expect(safe.success).toBe(true)

    expect(parsed.port).toBe(8080)
    expect(parsed.debug).toBe(true)
    expect(parsed.name).toBe('my-app')
    expect(parsed.version).toBe('1.0.0')
  })

  it('rejects non-zui schemas', async () => {
    const schema = {
      _output: undefined,
      safeParse: () => ({ success: true, data: { version: '1.0.0' } }),
    }

    await expect(
      zai
        .patch([configFile], 'change port to 8080 and set debug to true', {
          schema: schema as any,
        })
        .result()
    ).rejects.toThrow('@bpinternal/zui')
  })

  it('validates patched JSON against a nested zui schema', async () => {
    const schema = z.object({
      database: z.object({
        host: z.string(),
        port: z.number().min(5434),
        credentials: z.object({
          username: z.string(),
          password: z.string().min(1),
        }),
      }),
      features: z.array(z.string()),
    })

    const file: File = {
      path: 'config.json',
      name: 'config.json',
      content: JSON.stringify(
        {
          database: {
            host: 'localhost',
            port: 5432,
            credentials: { username: 'admin', password: 'secret' },
          },
          features: ['auth', 'logging'],
        },
        null,
        2
      ),
    }

    const result = await zai
      .patch(
        [file],
        'change database host to "db.prod.internal", port to 5433 or higher, and add "caching" to features',
        {
          schema,
        }
      )
      .result()

    expect(result.output).toHaveLength(1)
    const parsed = JSON.parse(result.output[0].content)

    const safe = schema.safeParse(parsed)
    expect(safe.success).toBe(true)

    expect(parsed.database.host).toBe('db.prod.internal')
    expect(parsed.database.port).toBe(5434)
    expect(parsed.database.credentials.username).toBe('admin')
    expect(parsed.database.credentials.password).toBe('secret')
    expect(parsed.features).toContain('auth')
    expect(parsed.features).toContain('logging')
    expect(parsed.features).toContain('caching')
  })
})
