// @vitest-environment jsdom
//
// The editor page. We mock `@voltro/client` (content.types fetch, the live list,
// the save mutation) and drive it: after the types load the page renders a type
// chip + an auto-generated <ContentForm> (real, from @voltro/cms/web), and a
// save that comes back `{ ok: false, violations }` annotates the form inline
// rather than throwing.

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { act, createElement, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { I18nProvider } from '@voltro/i18n'
import enCatalog from '../../locales/en'

const blogType = {
  name: 'blogPost', displayName: 'Blog post', pluralName: 'Blog posts',
  columns: ['title', 'slug', 'status'],
  fields: [
    { name: 'title', kind: 'string', editorHint: 'text', isOptional: false },
    { name: 'body', kind: 'richText', editorHint: 'richText', isOptional: false },
  ],
}

const typesRun = vi.fn<() => Promise<{ types: unknown }>>()
const getRun = vi.fn<() => Promise<{ row: unknown }>>()
const saveMutate = vi.fn<(input: unknown) => Promise<unknown>>()
const subData = vi.fn<() => { data: unknown }>()

vi.mock('@voltro/client', () => ({
  useAction: (_api: string, tag: string) => ({
    run: tag === 'content.types' ? typesRun : getRun,
    pending: false,
  }),
  useMutation: (_api: string, tag: string) => ({
    mutate: tag === 'content.saveDraft' ? saveMutate : vi.fn().mockResolvedValue({ ok: true }),
    pending: false,
  }),
  useSubscription: () => subData(),
}))

const { default: Editor, meta, renderMode } = await import('./page')

;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true

let container: HTMLDivElement
let root: Root

const render = (node: ReactNode): void => {
  container = document.createElement('div')
  document.body.appendChild(container)
  act(() => {
    root = createRoot(container)
    root.render(
      createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
    )
  })
}

// Flush the mount-effect fetch (typesRun.then → setState) + its re-render.
const flush = async (): Promise<void> => {
  await act(async () => { await Promise.resolve(); await Promise.resolve() })
}

beforeEach(() => {
  typesRun.mockReset(); typesRun.mockResolvedValue({ types: [blogType] })
  getRun.mockReset(); getRun.mockResolvedValue({ row: null })
  saveMutate.mockReset(); saveMutate.mockResolvedValue({ ok: true, id: 'blogPost_1', status: 'draft' })
  subData.mockReset(); subData.mockReturnValue({ data: [] })
})

afterEach(() => {
  if (root) act(() => root.unmount())
  container?.remove()
  document.body.innerHTML = ''
})

describe('editor — config', () => {
  test('is an SSR page titled Content (so the parent gate runs per request)', () => {
    expect(renderMode).toBe('ssr')
    expect(meta({ locale: 'en' }).title).toBe('Content')
  })
})

describe('editor — render', () => {
  test('after the types load, renders the type chip and an auto-generated form', async () => {
    render(createElement(Editor))
    await flush()
    expect(typesRun).toHaveBeenCalled()
    expect(container.textContent).toContain('Blog post')
    // <ContentForm> renders one [data-cms-field] per declared field.
    expect(container.querySelectorAll('[data-cms-field]').length).toBe(2)
    expect(container.querySelector('[data-cms-field="title"]')).not.toBeNull()
  })
})

describe('editor — validation', () => {
  test('a save that returns { ok: false, violations } annotates the form', async () => {
    saveMutate.mockResolvedValue({ ok: false, violations: [{ field: 'title', rule: 'required', message: 'missing required field' }] })
    render(createElement(Editor))
    await flush()
    const form = container.querySelector('form')!
    await act(async () => {
      form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
      await Promise.resolve(); await Promise.resolve()
    })
    expect(saveMutate).toHaveBeenCalled()
    expect(container.querySelector('.violations')).not.toBeNull()
    expect(container.textContent).toContain('missing required field')
  })
})
