// @vitest-environment jsdom
//
// The project note: ⌘N opens it, the text lands in <project>/.notes.txt, and it
// is written whenever the window loses focus.
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
import React from 'react'

vi.mock('../src/renderer/src/terminals', () => {
  let n = 0
  return { Terminal: () => null, lid: () => ++n }
})

import { App } from '../src/renderer/src/App'
import { ProjectProvider } from '../src/renderer/src/project'

let stored = ''
let writes: string[] = []

function stubBridge(): void {
  const noop = (): void => {}
  const off = (): (() => void) => noop
  ;(window as unknown as { helder: unknown }).helder = {
    platform: 'darwin',
    clipboard: { writeText: noop, readText: () => '' },
    project: {
      current: async () => ({ root: '/repo', name: 'repo' }),
      open: async () => ({ root: '/repo', name: 'repo' }),
      openPath: async () => ({ root: '/repo', name: 'repo' }),
      recent: async () => [],
    },
    fs: {
      tree: async () => ({ name: 'repo', type: 'dir', path: '', children: [] }),
      readDir: async () => [], files: async () => ({}), read: async () => '',
      imageDataUrl: async () => '', write: async () => {}, delete: async () => {},
      create: async () => {}, mkdir: async () => {},
    },
    shell: { reveal: noop },
    notes: {
      read: async () => stored,
      write: async (t: string) => { stored = t; writes.push(t) },
    },
    git: {
      load: async () => ({ branch: 'main', changes: [] }),
      stage: async () => {}, unstage: async () => {}, commit: async () => {},
      push: async () => ({ ok: true, message: '' }), discard: async () => {},
    },
    pty: { available: async () => false, create: async () => 1, write: noop, resize: noop, kill: noop, onData: off, onExit: off },
    config: { get: async () => (await import('../src/renderer/src/types')).DEFAULT_CONFIG, theme: async () => '' },
    recent: { get: async () => [], set: async () => {} },
    search: { content: async () => [], files: async () => [] },
    dialog: { unsavedClose: async () => 'cancel' },
    log: { write: noop, path: async () => null, open: async () => {}, reveal: async () => {} },
    onProjectChanged: off, onConfigChanged: off, onRefresh: off,
  }
}

beforeAll(() => {
  globalThis.ResizeObserver = class { observe(): void {} unobserve(): void {} disconnect(): void {} } as never
  if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = (): void => {}
})
beforeEach(() => { stored = ''; writes = []; stubBridge() })
afterEach(() => {
  cleanup()
  localStorage.clear()
  delete (window as unknown as { helder?: unknown }).helder
})

async function boot(): Promise<HTMLElement> {
  const c = render(<ProjectProvider><App /></ProjectProvider>).container
  await waitFor(() => { if (!c.querySelector('.git-foot')) throw new Error('not ready') })
  return c
}

function pressCmdN(): void {
  act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'n', metaKey: true })) })
}

/** Open the note. The shortcut is registered in an effect, which React may not
 * have flushed yet when the first paint lands, so keep pressing until it takes.
 * A second ⌘N with the overlay already open is a no-op. */
async function openNote(c: HTMLElement): Promise<HTMLTextAreaElement> {
  await waitFor(() => {
    pressCmdN()
    if (!c.querySelector('.notes-modal')) throw new Error('note not open')
  })
  return c.querySelector('.notes-input') as HTMLTextAreaElement
}

function blurWindow(): void {
  act(() => { window.dispatchEvent(new Event('blur')) })
}

describe('project note', () => {
  it('⌘N opens the note overlay', async () => {
    const c = await boot()
    expect(c.querySelector('.notes-modal')).toBeNull()
    await openNote(c)
    expect(c.querySelector('.notes-modal .notes-file')?.textContent).toBe('.notes.txt')
  })

  it('loads the note that is already on disk', async () => {
    stored = 'earlier thoughts\n'
    const c = await boot()
    const ta = await openNote(c)
    await waitFor(() => expect(ta.value).toBe('earlier thoughts\n'))
  })

  it('writes the note when the window loses focus', async () => {
    const c = await boot()
    const ta = await openNote(c)
    fireEvent.change(ta, { target: { value: 'buy milk' } })

    expect(writes).toEqual([])   // nothing written while typing
    blurWindow()
    await waitFor(() => expect(writes).toEqual(['buy milk']))
    expect(stored).toBe('buy milk')
  })

  it('does not rewrite the file when nothing changed', async () => {
    stored = 'unchanged'
    await boot()
    await waitFor(() => expect(stored).toBe('unchanged'))
    blurWindow()
    blurWindow()
    expect(writes).toEqual([])
  })

  it('Esc closes the note and saves it right away', async () => {
    const c = await boot()
    const ta = await openNote(c)
    fireEvent.change(ta, { target: { value: 'quick capture' } })

    act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) })
    await waitFor(() => expect(c.querySelector('.notes-modal')).toBeNull())
    await waitFor(() => expect(writes).toEqual(['quick capture']))
  })

  it('⌘P passes the note to the agent, then saves and closes it', async () => {
    const c = await boot()
    const ta = await openNote(c)
    fireEvent.change(ta, { target: { value: 'refactor the policy' } })

    const seen: string[] = []
    const onPaste = (e: Event): void => { seen.push((e as CustomEvent<string>).detail) }
    window.addEventListener('agentPaste', onPaste)
    act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'p', metaKey: true })) })
    window.removeEventListener('agentPaste', onPaste)

    expect(seen).toEqual(['refactor the policy'])
    await waitFor(() => expect(c.querySelector('.notes-modal')).toBeNull())
    await waitFor(() => expect(writes).toEqual(['refactor the policy']))
  })

  it('⌘P does nothing when the note is empty', async () => {
    const c = await boot()
    await openNote(c)

    const seen: string[] = []
    const onPaste = (e: Event): void => { seen.push((e as CustomEvent<string>).detail) }
    window.addEventListener('agentPaste', onPaste)
    act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'p', metaKey: true })) })
    window.removeEventListener('agentPaste', onPaste)

    expect(seen).toEqual([])
    expect(c.querySelector('.notes-modal')).not.toBeNull()
  })

  it('the header shows the pass-to-agent hint', async () => {
    const c = await boot()
    await openNote(c)
    const hint = c.querySelector('.notes-modal .notes-hint')
    expect(hint?.textContent).toContain('To agent')
    expect(hint?.querySelector('kbd')?.textContent).toBe('⌘P')
  })

  it('the title bar carries the ⌘N note action', async () => {
    const c = await boot()
    const btn = [...c.querySelectorAll('.titlebar .tb-btn')]
      .find((b) => b.textContent?.includes('Note')) as HTMLButtonElement | undefined
    expect(btn?.querySelector('kbd')?.textContent).toBe('⌘N')
    expect(btn?.className).not.toContain('on')

    act(() => { btn?.click() })
    await waitFor(() => expect(c.querySelector('.notes-modal')).not.toBeNull())
    // The action reads as "on" while the note is open, like the other toggles.
    expect(btn?.className).toContain('on')
  })

  it('keeps the text when reopened', async () => {
    const c = await boot()
    const ta = await openNote(c)
    fireEvent.change(ta, { target: { value: 'still here' } })
    act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) })
    await waitFor(() => expect(c.querySelector('.notes-modal')).toBeNull())

    const again = await openNote(c)
    expect(again.value).toBe('still here')
  })
})
