import { EventEmitter } from 'events'
import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('@logger', () => ({
  loggerService: {
    withContext: () => ({
      debug: vi.fn(),
      error: vi.fn(),
      warn: vi.fn()
    })
  }
}))

vi.mock('@data/PreferenceService', async () => {
  const { MockMainPreferenceServiceExport } = await import('@test-mocks/main/PreferenceService')
  return MockMainPreferenceServiceExport
})

const {
  windowServiceMock,
  windowManagerMock,
  selectionServiceMock,
  settingsWindowServiceMock,
  quickAssistantServiceMock,
  commandServiceMock,
  globalShortcutMock
} = vi.hoisted(() => ({
  windowServiceMock: {
    onMainWindowCreated: vi.fn(),
    showMainWindow: vi.fn(),
    toggleMainWindow: vi.fn()
  },
  windowManagerMock: {
    open: vi.fn(),
    broadcastToType: vi.fn()
  },
  selectionServiceMock: {
    toggleEnabled: vi.fn(),
    processSelectTextByShortcut: vi.fn()
  },
  settingsWindowServiceMock: {
    open: vi.fn()
  },
  quickAssistantServiceMock: {
    toggleQuickAssistant: vi.fn()
  },
  commandServiceMock: {
    execute: vi.fn()
  },
  globalShortcutMock: {
    register: vi.fn(),
    unregister: vi.fn()
  }
}))

vi.mock('@application', async () => {
  const { mockApplicationFactory } = await import('@test-mocks/main/application')
  return mockApplicationFactory({
    MainWindowService: windowServiceMock,
    WindowManager: windowManagerMock,
    SelectionService: selectionServiceMock,
    SettingsWindowService: settingsWindowServiceMock,
    QuickAssistantService: quickAssistantServiceMock,
    CommandService: commandServiceMock
  } as any)
})

vi.mock('@main/core/lifecycle', () => {
  class MockBaseService {
    protected readonly _disposables: Array<{ dispose: () => void } | (() => void)> = []

    protected registerDisposable<T extends { dispose: () => void } | (() => void)>(disposable: T): T {
      this._disposables.push(disposable)
      return disposable
    }
  }

  return {
    BaseService: MockBaseService,
    Injectable: () => (target: unknown) => target,
    ServicePhase: () => (target: unknown) => target,
    DependsOn: () => (target: unknown) => target,
    Phase: { WhenReady: 'whenReady' }
  }
})

vi.mock('electron', () => ({
  globalShortcut: globalShortcutMock
}))

import { WindowType } from '@main/core/window/types'
import { IpcChannel } from '@shared/IpcChannel'
import { MockMainPreferenceServiceUtils } from '@test-mocks/main/PreferenceService'

import { ShortcutService } from '../ShortcutService'

// Mirrors the selection commands' supportedPlatforms (darwin/win32/linux) — SelectionService supports linux too.
const supportsSelectionShortcuts = ['darwin', 'win32', 'linux'].includes(process.platform)
const settingsShortcutHandledByNativeMenu = process.platform === 'darwin'

class MockBrowserWindow {
  private readonly events = new EventEmitter()
  private readonly webContentsEvents = new EventEmitter()
  private destroyed = false
  private focused = true

  public readonly webContents = {
    send: vi.fn(),
    isLoadingMainFrame: vi.fn(() => false),
    once: vi.fn((event: string, callback: (...args: any[]) => void) => {
      this.webContentsEvents.once(event, callback)
    })
  }

  public readonly on = vi.fn((event: string, callback: (...args: any[]) => void) => {
    this.events.on(event, callback)
    return this
  })

  public readonly once = vi.fn((event: string, callback: (...args: any[]) => void) => {
    this.events.once(event, callback)
    return this
  })

  public readonly off = vi.fn((event: string, callback: (...args: any[]) => void) => {
    this.events.off(event, callback)
    return this
  })

  public readonly isDestroyed = vi.fn(() => this.destroyed)
  public readonly isFocused = vi.fn(() => this.focused)
  public readonly isMinimized = vi.fn(() => false)
  public readonly isVisible = vi.fn(() => true)

  public emit(event: string, ...args: any[]) {
    this.events.emit(event, ...args)
  }

  public emitWebContents(event: string, ...args: any[]) {
    this.webContentsEvents.emit(event, ...args)
  }

  public setFocused(value: boolean) {
    this.focused = value
  }

  public destroy() {
    this.destroyed = true
  }
}

describe('ShortcutService', () => {
  let service: ShortcutService
  let mainWindow: MockBrowserWindow
  let currentMainWindow: MockBrowserWindow

  beforeEach(() => {
    vi.clearAllMocks()
    MockMainPreferenceServiceUtils.resetMocks()

    mainWindow = new MockBrowserWindow()
    currentMainWindow = mainWindow
    // Production flow: MainWindowService fires onMainWindowCreated after the window is ready.
    // Tests exercise the same path by firing the callback synchronously on subscribe.
    // Tests that simulate a service restart can reassign `currentMainWindow` before the second onInit.
    windowServiceMock.onMainWindowCreated.mockImplementation((callback: (window: MockBrowserWindow) => void) => {
      callback(currentMainWindow)
      return { dispose: vi.fn() }
    })

    globalShortcutMock.register.mockReturnValue(true)

    service = new ShortcutService()
  })

  it('registers focused window shortcuts including shortcut variants', async () => {
    await (service as any).onInit()

    if (settingsShortcutHandledByNativeMenu) {
      expect(globalShortcutMock.register).not.toHaveBeenCalledWith('CommandOrControl+,', expect.any(Function))
    } else {
      expect(globalShortcutMock.register).toHaveBeenCalledWith('CommandOrControl+,', expect.any(Function))
    }
    expect(globalShortcutMock.register).toHaveBeenCalledWith('CommandOrControl+=', expect.any(Function))
    expect(globalShortcutMock.register).toHaveBeenCalledWith('CommandOrControl+numadd', expect.any(Function))
  })

  it('registers global shortcuts immediately for an unfocused main window', async () => {
    MockMainPreferenceServiceUtils.setPreferenceValue('shortcut.app.window.show', {
      binding: ['CommandOrControl', 'M'],
      enabled: true
    })
    mainWindow.setFocused(false)

    await (service as any).onInit()

    expect(globalShortcutMock.register).toHaveBeenCalledWith('CommandOrControl+M', expect.any(Function))
    expect(globalShortcutMock.register).not.toHaveBeenCalledWith('CommandOrControl+=', expect.any(Function))

    const showMainRegistration = globalShortcutMock.register.mock.calls.find(
      ([accelerator]) => accelerator === 'CommandOrControl+M'
    )
    const showMainHandler = showMainRegistration?.[1] as (() => void) | undefined
    showMainHandler?.()

    expect(commandServiceMock.execute).toHaveBeenCalledWith('app.window.show', mainWindow)
  })

  it('opens the settings window through SettingsWindowService', async () => {
    await (service as any).onInit()

    const handler = (service as any).handlers.get('app.settings.open') as (() => void) | undefined
    handler?.()

    expect(commandServiceMock.execute).toHaveBeenCalledWith('app.settings.open', undefined)
    expect(settingsWindowServiceMock.open).not.toHaveBeenCalled()
    expect(windowServiceMock.showMainWindow).not.toHaveBeenCalled()
  })

  it('re-registers only the changed accelerator when shortcut binding changes', async () => {
    await (service as any).onInit()
    globalShortcutMock.register.mockClear()
    globalShortcutMock.unregister.mockClear()

    MockMainPreferenceServiceUtils.setPreferenceValue('shortcut.app.zoom.in', {
      binding: ['Alt', '='],
      enabled: true
    })

    expect(globalShortcutMock.unregister).toHaveBeenCalledWith('CommandOrControl+=')
    expect(globalShortcutMock.register).toHaveBeenCalledWith('Alt+=', expect.any(Function))
    expect(globalShortcutMock.register).not.toHaveBeenCalledWith('CommandOrControl+=', expect.any(Function))
  })

  it('reacts to quick assistant enablement changes for quick assistant shortcut', async () => {
    MockMainPreferenceServiceUtils.setPreferenceValue('shortcut.quick_assistant.toggle', {
      binding: ['CommandOrControl', 'E'],
      enabled: true
    })
    MockMainPreferenceServiceUtils.setPreferenceValue('feature.quick_assistant.enabled', false)

    await (service as any).onInit()

    expect(globalShortcutMock.register).not.toHaveBeenCalledWith('CommandOrControl+E', expect.any(Function))

    globalShortcutMock.register.mockClear()
    MockMainPreferenceServiceUtils.setPreferenceValue('feature.quick_assistant.enabled', true)

    expect(globalShortcutMock.register).toHaveBeenCalledWith('CommandOrControl+E', expect.any(Function))
  })

  it('reacts to selection assistant enablement changes for selection shortcuts', async () => {
    MockMainPreferenceServiceUtils.setPreferenceValue('shortcut.selection.toggle', {
      binding: ['CommandOrControl', 'Shift', 'S'],
      enabled: true
    })
    MockMainPreferenceServiceUtils.setPreferenceValue('feature.selection.enabled', false)

    await (service as any).onInit()

    expect(globalShortcutMock.register).not.toHaveBeenCalledWith('CommandOrControl+Shift+S', expect.any(Function))

    globalShortcutMock.register.mockClear()
    MockMainPreferenceServiceUtils.setPreferenceValue('feature.selection.enabled', true)

    if (supportsSelectionShortcuts) {
      expect(globalShortcutMock.register).toHaveBeenCalledWith('CommandOrControl+Shift+S', expect.any(Function))
    } else {
      expect(globalShortcutMock.register).not.toHaveBeenCalledWith('CommandOrControl+Shift+S', expect.any(Function))
    }
  })

  it('re-registers window-bound shortcuts when the main window instance changes', async () => {
    await (service as any).onInit()

    const nextWindow = new MockBrowserWindow()
    globalShortcutMock.register.mockClear()
    globalShortcutMock.unregister.mockClear()

    ;(service as any).registerForWindow(nextWindow)

    expect(globalShortcutMock.unregister).toHaveBeenCalledWith('CommandOrControl+=')

    const zoomInRegistration = globalShortcutMock.register.mock.calls.find(
      ([accelerator]) => accelerator === 'CommandOrControl+='
    )
    expect(zoomInRegistration).toBeTruthy()

    const zoomInHandler = zoomInRegistration?.[1] as (() => void) | undefined
    zoomInHandler?.()

    expect(commandServiceMock.execute).toHaveBeenCalledWith('app.zoom.in', nextWindow)
  })

  it('resets boot registration state when the service stops and starts again', async () => {
    await (service as any).onInit()
    await (service as any).onStop()

    const nextWindow = new MockBrowserWindow()
    currentMainWindow = nextWindow

    await (service as any).onInit()

    expect(nextWindow.once).toHaveBeenCalledWith('ready-to-show', expect.any(Function))
  })

  it('notifies the renderer when a shortcut cannot be registered', async () => {
    globalShortcutMock.register.mockImplementation((accelerator: string) => accelerator !== 'CommandOrControl+0')

    await (service as any).onInit()

    expect(windowManagerMock.broadcastToType).toHaveBeenCalledWith(
      WindowType.Main,
      IpcChannel.Shortcut_RegistrationConflict,
      {
        key: 'shortcut.app.zoom.reset',
        accelerator: 'CommandOrControl+0',
        hasConflict: true
      }
    )
  })

  it('does not notify repeatedly for the same shortcut conflict', async () => {
    globalShortcutMock.register.mockImplementation((accelerator: string) => accelerator !== 'CommandOrControl+0')

    await (service as any).onInit()
    windowManagerMock.broadcastToType.mockClear()

    ;(service as any).reregisterShortcuts()

    expect(windowManagerMock.broadcastToType).not.toHaveBeenCalledWith(
      WindowType.Main,
      IpcChannel.Shortcut_RegistrationConflict,
      expect.objectContaining({
        key: 'shortcut.app.zoom.reset',
        hasConflict: true
      })
    )
  })
})
