import { describe, expect, it, vi } from 'vitest'

vi.mock('@opentiny/vue-icon', () => ({
  default: {
    IconWarning: {}
  }
}))

import {
  fixComponentName,
  fixMethods,
  getJsonObjectString,
  isPlainObject,
  isValidFastJsonPatch,
  isValidJsonPatchObjectString,
  isValidOperation,
  isValidSchemaChildren,
  jsonPatchAutoFix,
  schemaAutoFix
} from '../../src/utils/schema.utils'

const createComponent = (componentName = 'div', children: any[] = []) => ({
  componentName,
  props: {},
  children
})

describe('schema utils', () => {
  describe('isPlainObject', () => {
    it('accepts regular object literals', () => {
      expect(isPlainObject({})).toBe(true)
      expect(isPlainObject({ componentName: 'TinyButton' })).toBe(true)
    })

    it('rejects arrays, null, and primitive values', () => {
      expect(isPlainObject([])).toBe(false)
      expect(isPlainObject(null)).toBe(false)
      expect(isPlainObject('schema')).toBe(false)
      expect(isPlainObject(1)).toBe(false)
      expect(isPlainObject(false)).toBe(false)
    })
  })

  describe('getJsonObjectString', () => {
    it('extracts json from fenced markdown blocks', () => {
      const content = [
        'AI result:',
        '```json',
        '[{"op":"add","path":"/children/0","value":{"componentName":"div"}}]',
        '```',
        'done'
      ].join('\n')

      expect(getJsonObjectString(content).trim()).toBe(
        '[{"op":"add","path":"/children/0","value":{"componentName":"div"}}]'
      )
    })

    it('extracts json from schema fenced markdown blocks', () => {
      const content = ['```schema', '[{"op":"replace","path":"/css","value":".foo{color:red}"}]', '```'].join('\n')

      expect(getJsonObjectString(content).trim()).toBe('[{"op":"replace","path":"/css","value":".foo{color:red}"}]')
    })

    it('extracts the json patch array from surrounding prose', () => {
      const content =
        '好的,以下是更新内容:[{"op":"add","path":"/children/0","value":{"componentName":"TinyForm"}}],页面已生成。'

      expect(getJsonObjectString(content)).toBe(
        '[{"op":"add","path":"/children/0","value":{"componentName":"TinyForm"}}]'
      )
    })

    it('returns the original content when no json array is present', () => {
      const content = 'no patch content'

      expect(getJsonObjectString(content)).toBe(content)
    })

    it('uses the outermost array when prose contains nested arrays in values', () => {
      const content =
        'patch: [{"op":"add","path":"/children/0","value":{"componentName":"TinySelect","props":{"options":[1,2,3]}}}] end'

      expect(getJsonObjectString(content)).toBe(
        '[{"op":"add","path":"/children/0","value":{"componentName":"TinySelect","props":{"options":[1,2,3]}}}]'
      )
    })
  })

  describe('isValidOperation', () => {
    it('validates add, replace, and test operations with value', () => {
      expect(isValidOperation({ op: 'add', path: '/children/0', value: createComponent() })).toBe(true)
      expect(isValidOperation({ op: 'replace', path: '/css', value: '.page{}' })).toBe(true)
      expect(isValidOperation({ op: 'test', path: '/componentName', value: 'Page' })).toBe(true)
    })

    it('validates move and copy operations with from', () => {
      expect(isValidOperation({ op: 'move', path: '/children/1', from: '/children/0' })).toBe(true)
      expect(isValidOperation({ op: 'copy', path: '/children/1', from: '/children/0' })).toBe(true)
    })

    it('allows internal _get operations used by the robot prompt flow', () => {
      expect(isValidOperation({ op: '_get', path: '/children' })).toBe(true)
    })

    it('rejects operations with unsupported op, missing path, or missing payload', () => {
      expect(isValidOperation(null)).toBe(false)
      expect(isValidOperation('patch')).toBe(false)
      expect(isValidOperation({ op: 'merge', path: '/children' })).toBe(false)
      expect(isValidOperation({ op: 'add', value: createComponent() })).toBe(false)
      expect(isValidOperation({ op: 'replace', path: '/css' })).toBe(false)
      expect(isValidOperation({ op: 'copy', path: '/children/1' })).toBe(false)
    })
  })

  describe('isValidFastJsonPatch', () => {
    it('accepts a single operation or an operation array', () => {
      const patch = { op: 'add', path: '/children/0', value: createComponent() }

      expect(isValidFastJsonPatch(patch)).toBe(true)
      expect(isValidFastJsonPatch([patch])).toBe(true)
    })

    it('rejects arrays containing invalid operations', () => {
      expect(
        isValidFastJsonPatch([
          { op: 'add', path: '/children/0', value: createComponent() },
          { op: 'replace', path: '/css' }
        ])
      ).toBe(false)
    })
  })

  describe('jsonPatchAutoFix', () => {
    it('keeps all valid patches for final updates', () => {
      const patches = [
        { op: 'add', path: '/children/0', value: createComponent('TinyForm') },
        { op: 'replace', path: '/state/title', value: 'Survey' },
        { op: 'replace', path: '/css', value: '.survey{}' }
      ]

      expect(jsonPatchAutoFix(patches, true)).toEqual(patches)
    })

    it('keeps complete leading patches while streaming', () => {
      const patches = [
        { op: 'replace', path: '/state/title', value: 'Survey' },
        { op: 'add', path: '/children/0', value: createComponent('TinyForm') }
      ]

      expect(jsonPatchAutoFix(patches, false)).toEqual(patches)
    })

    it('drops the last streaming patch when it is not a children patch', () => {
      const patches = [
        { op: 'add', path: '/children/0', value: createComponent('TinyForm') },
        { op: 'replace', path: '/css', value: '.partial{' }
      ]

      expect(jsonPatchAutoFix(patches, false)).toEqual([patches[0]])
    })

    it('filters invalid operations after applying the streaming last-patch rule', () => {
      const validPatch = { op: 'add', path: '/children/0', value: createComponent('TinyForm') }
      const invalidPatch = { op: 'replace', path: '/css' }

      expect(jsonPatchAutoFix([validPatch, invalidPatch], true)).toEqual([validPatch])
    })

    it('keeps a trailing streaming children patch because it can render progressively', () => {
      const patches = [
        { op: 'replace', path: '/state/title', value: 'Survey' },
        { op: 'add', path: '/children/1', value: createComponent('TinyButton') }
      ]

      expect(jsonPatchAutoFix(patches, false)).toEqual(patches)
    })
  })

  describe('isValidSchemaChildren', () => {
    it('accepts missing children and arrays of schema nodes', () => {
      expect(isValidSchemaChildren(undefined)).toBe(true)
      expect(isValidSchemaChildren([createComponent('TinyForm')])).toBe(true)
      expect(isValidSchemaChildren([createComponent('TinyForm', [createComponent('TinyInput')])])).toBe(true)
    })

    it('rejects string nodes that would break canvas node-map generation', () => {
      expect(isValidSchemaChildren(['broken node'])).toBe(false)
      expect(isValidSchemaChildren([createComponent('TinyForm', ['broken child'])])).toBe(false)
    })

    it('rejects null and primitive nested children while allowing empty arrays', () => {
      expect(isValidSchemaChildren([createComponent('TinyForm', [])])).toBe(true)
      expect(isValidSchemaChildren([createComponent('TinyForm', [null])])).toBe(false)
      expect(isValidSchemaChildren([createComponent('TinyForm', [1])])).toBe(false)
      expect(isValidSchemaChildren([createComponent('TinyForm', [false])])).toBe(false)
    })
  })

  describe('isValidJsonPatchObjectString', () => {
    it('accepts valid json patch content', () => {
      const result = isValidJsonPatchObjectString(
        '[{"op":"add","path":"/children/0","value":{"componentName":"TinyButton"}}]'
      )

      expect(result.isError).toBe(false)
    })

    it('repairs slightly incomplete json before validating', () => {
      const result = isValidJsonPatchObjectString(
        '[{"op":"add","path":"/children/0","value":{"componentName":"TinyButton"}}'
      )

      expect(result.isError).toBe(false)
    })

    it('rejects json that is not a valid patch operation', () => {
      const result = isValidJsonPatchObjectString('[{"path":"/children/0"}]')

      expect(result.isError).toBe(true)
    })
  })

  describe('schemaAutoFix', () => {
    it('adds a default component name to plain schema nodes', () => {
      const node = { props: {}, children: [{ props: {} }] }

      schemaAutoFix(node)

      expect(node.componentName).toBe('div')
      expect(node.children[0].componentName).toBe('div')
    })

    it('does not treat json patch operations as schema nodes', () => {
      const patch = { op: 'add', path: '/children/0', value: createComponent() }

      schemaAutoFix(patch)

      expect(patch).not.toHaveProperty('componentName')
    })

    it('recursively keeps existing component names when fixing children', () => {
      const node = createComponent('TinyForm', [createComponent('TinyFormItem'), { props: {} }])

      schemaAutoFix(node)

      expect(node.componentName).toBe('TinyForm')
      expect(node.children[0].componentName).toBe('TinyFormItem')
      expect(node.children[1].componentName).toBe('div')
    })
  })

  describe('fixMethods', () => {
    it('keeps valid JSFunction methods untouched', () => {
      const methods = {
        submit: {
          type: 'JSFunction',
          value: 'function submit() {\n  return true;\n}'
        }
      }

      fixMethods(methods)

      expect(methods.submit.value).toContain('return true')
    })

    it('replaces invalid method definitions with safe placeholders', () => {
      const methods = {
        submit: {
          type: 'JSExpression',
          value: 'this.submit()'
        }
      }

      fixMethods(methods)

      expect(methods.submit.type).toBe('JSFunction')
      expect(methods.submit.value).toContain('function submit()')
    })
  })
})