// @vitest-environment jsdom
//
// `/` — the reactive notes page, driven through `makeVoltroTestClient`
// (`@voltro/testing/client`) rather than a `vi.mock` of anything.
//
// WHY THE HARNESS HERE. The old suite faked `src/lib/api` — the app's own typed
// binding — with the argument that mocking `@voltro/client` underneath would be
// the wrong seam. That argument was right about the seam and reached the wrong
// conclusion, because the harness does not mock `@voltro/client` at all: it
// PROVIDES the runtime context the real hooks read. So `createHooks` runs, the
// real `useSubscription` / `useMutation` run, and the fake stops at the
// transport — one layer below the binding rather than on top of it.
//
// What that buys, concretely: the page is the framework's flagship example of
// the reactive loop, and every "different data" case here used to be a fresh
// mount with a different mock return. Nothing pushed a delta into a MOUNTED
// tree, which is the one behaviour the page exists to demonstrate. The last two
// cases do, via `setSubscription`.

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

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

const LIST = 'notes.list'

let container: HTMLDivElement
let root: Root
let client: VoltroTestClient

const note = (over: Partial<{ id: string; title: string; body: string; optimistic: boolean }> = {}) => ({
  id: over.id ?? 'n_1',
  title: over.title ?? 'first',
  body: over.body ?? '',
  done: false,
  tenantId: 'acme',
  createdAt: new Date(0),
  ...(over.optimistic === true ? { optimistic: true } : {}),
})

/** `notes.create` resolves by default; a case that needs the failure path
 *  overrides it. The harness records every call on `client.calls`. */
const harness = (
  subscriptions: Readonly<Record<string, unknown>> = {},
  create: (input: unknown) => unknown = () => undefined,
): VoltroTestClient =>
  makeVoltroTestClient({ subscriptions, mutations: { 'notes.create': create } })

// The page uses <T>/useTFn, so it needs an <I18nProvider>. Wrap with the real
// English catalog (the framework auto-wires this provider in the running app).
const render = (): void => {
  container = document.createElement('div')
  document.body.appendChild(container)
  act(() => {
    root = createRoot(container)
    root.render(
      createElement(I18nProvider, {
        locale: 'en',
        messages: enCatalog,
        defaultLocale: 'en',
        children: createElement(client.Provider, null, createElement(IndexPage) as ReactNode),
      }),
    )
  })
}

const setInput = (el: HTMLInputElement | HTMLTextAreaElement, value: string): void => {
  const proto =
    el instanceof window.HTMLTextAreaElement
      ? window.HTMLTextAreaElement.prototype
      : window.HTMLInputElement.prototype
  const setter = Object.getOwnPropertyDescriptor(proto, 'value')!.set!
  act(() => {
    setter.call(el, value)
    el.dispatchEvent(new Event('input', { bubbles: true }))
  })
}

const submit = async (): Promise<void> => {
  const form = container.querySelector('form') as HTMLFormElement
  await act(async () => {
    form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
  })
}

beforeEach(() => { client = harness() })

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

describe('index — subscription states', () => {
  test('shows the loading hint before the first snapshot', () => {
    // A tag with no fixture IS the cold state — no stub had to invent one.
    render()
    expect(container.textContent).toContain('loading…')
    expect(container.querySelectorAll('li')).toHaveLength(0)
  })

  test('shows the empty state once a snapshot arrives with no rows', () => {
    client = harness({ [LIST]: [] })
    render()
    expect(container.textContent).toContain('No notes yet')
  })

  test('renders each note with its title and body', () => {
    client = harness({ [LIST]: [note({ id: 'n_1', title: 'first', body: 'body one' }), note({ id: 'n_2', title: 'second' })] })
    render()
    expect(container.querySelectorAll('li')).toHaveLength(2)
    expect(container.textContent).toContain('first')
    expect(container.textContent).toContain('body one')
    expect(container.textContent).toContain('second')
  })

  test('renders optimistic rows faintly + italicised, with the pending count', () => {
    client = harness({ [LIST]: [] })
    render()
    act(() => {
      client.setSubscription(LIST, [note({ id: 'n_opt', title: 'pending', optimistic: true })], { pendingPatches: 1 })
    })
    const li = container.querySelector('li')!
    expect(li.style.opacity).toBe('0.5')
    expect(li.style.fontStyle).toBe('italic')
    expect(container.textContent).toContain('1 pending')
  })

  test('renders the subscription error banner when the stream fails', () => {
    render()
    act(() => { client.failSubscription(LIST, 'boom') })
    expect(container.textContent).toContain('subscription error')
    expect(container.textContent).toContain('boom')
  })
})

describe('index — create form', () => {
  test('the Add button is disabled until a non-empty title is typed', () => {
    client = harness({ [LIST]: [] })
    render()
    const button = container.querySelector('button[type="submit"]') as HTMLButtonElement
    expect(button.disabled).toBe(true)
    setInput(container.querySelector('input') as HTMLInputElement, 'a note')
    expect(button.disabled).toBe(false)
  })

  test('submitting calls notes.create with the trimmed title + body + tenant, then clears', async () => {
    client = harness({ [LIST]: [] })
    render()
    setInput(container.querySelector('input') as HTMLInputElement, '  my title  ')
    setInput(container.querySelector('textarea') as HTMLTextAreaElement, '  my body  ')
    await submit()

    expect(client.calls).toEqual([
      { kind: 'mutation', tag: 'notes.create', input: { tenantId: 'acme', title: 'my title', body: 'my body' } },
    ])
    expect((container.querySelector('input') as HTMLInputElement).value).toBe('')
    expect((container.querySelector('textarea') as HTMLTextAreaElement).value).toBe('')
  })

  test('a whitespace-only title does not fire a mutation', async () => {
    client = harness({ [LIST]: [] })
    render()
    setInput(container.querySelector('input') as HTMLInputElement, '   ')
    await submit()
    expect(client.calls).toEqual([])
  })

  test('a REJECTED create leaves the form cleared and the list untouched', async () => {
    client = harness({ [LIST]: [] }, () => { throw new Error('TenantMismatch') })
    render()
    setInput(container.querySelector('input') as HTMLInputElement, 'nope')
    await submit()
    expect(client.calls).toHaveLength(1)
    expect(container.textContent).toContain('No notes yet')
  })
})

// The reactive loop itself — a server delta into a MOUNTED tree. This is what
// the page is a demonstration of, and what a per-mount mock cannot express.
describe('index — the reactive loop', () => {
  test('a delta pushed by the server re-renders the list in place', () => {
    client = harness({ [LIST]: [note({ id: 'n_1', title: 'first' })] })
    render()
    expect(container.querySelectorAll('li')).toHaveLength(1)

    act(() => {
      client.setSubscription(LIST, [note({ id: 'n_1', title: 'first' }), note({ id: 'n_2', title: 'second' })])
    })

    expect(container.querySelectorAll('li')).toHaveLength(2)
    expect(container.textContent).toContain('second')
  })

  test('the confirming delta clears the optimistic styling AND the pending count', () => {
    client = harness({ [LIST]: [] })
    render()
    act(() => {
      client.setSubscription(LIST, [note({ id: 'n_tmp', title: 'mine', optimistic: true })], { pendingPatches: 1 })
    })
    expect(container.querySelector('li')!.style.opacity).toBe('0.5')
    expect(container.textContent).toContain('1 pending')

    // The server confirms: the real row replaces the optimistic one.
    act(() => { client.setSubscription(LIST, [note({ id: 'n_1', title: 'mine' })]) })

    expect(container.querySelector('li')!.style.opacity).toBe('1')
    expect(container.textContent).not.toContain('1 pending')
  })
})
