import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { invoke } from '@tauri-apps/api/core'
import type { Settings } from '../types'
import {
  GITIGNORED_VISIBILITY_CHANGED_EVENT,
  TOGGLE_GITIGNORED_VISIBILITY_EVENT,
} from '../lib/gitignoredVisibilityEvents'
import { useSettings } from './useSettings'

const { trackEventMock } = vi.hoisted(() => ({
  trackEventMock: vi.fn(),
}))

const defaultSettings: Settings = {
  auto_pull_interval_minutes: null,
  git_enabled: null,
  git_path: null,
  git_provider: null,
  git_wsl_distro: null,
  autogit_enabled: null,
  autogit_idle_threshold_seconds: null,
  autogit_inactive_threshold_seconds: null,
  auto_advance_inbox_after_organize: null,
  telemetry_consent: null,
  crash_reporting_enabled: null,
  analytics_enabled: null,
  anonymous_id: null,
  release_channel: null,
  automatic_update_checks_enabled: null,
  theme_mode: null,
  ui_language: null,
  date_display_format: null,
  note_width_mode: null,
  sidebar_type_pluralization_enabled: null,
  ai_features_enabled: null,
  default_ai_agent: null,
  default_ai_target: null,
  ai_model_providers: null,
  ai_workspace_conversations: null,
  hide_gitignored_files: null,
  multi_workspace_enabled: null,
  all_notes_show_pdfs: null,
  all_notes_show_images: null,
  all_notes_show_unsupported: null,
}

const savedSettings: Settings = {
  auto_pull_interval_minutes: 15,
  git_enabled: null,
  git_path: null,
  git_provider: null,
  git_wsl_distro: null,
  autogit_enabled: true,
  autogit_idle_threshold_seconds: 90,
  autogit_inactive_threshold_seconds: 30,
  auto_advance_inbox_after_organize: true,
  telemetry_consent: null,
  crash_reporting_enabled: null,
  analytics_enabled: null,
  anonymous_id: null,
  release_channel: null,
  automatic_update_checks_enabled: null,
  theme_mode: null,
  ui_language: null,
  date_display_format: null,
  note_width_mode: null,
  sidebar_type_pluralization_enabled: null,
  ai_features_enabled: null,
  default_ai_agent: null,
  default_ai_target: null,
  ai_model_providers: null,
  ai_workspace_conversations: null,
  hide_gitignored_files: null,
  multi_workspace_enabled: null,
  all_notes_show_pdfs: null,
  all_notes_show_images: null,
  all_notes_show_unsupported: null,
}

let mockSettingsStore: Settings = { ...defaultSettings }

const mockInvokeFn = vi.fn((cmd: string, args?: Record<string, unknown>): Promise<unknown> => {
  if (cmd === 'get_settings') return Promise.resolve({ ...mockSettingsStore })
  if (cmd === 'save_settings') {
    mockSettingsStore = { ...(args as { settings: Settings }).settings }
    return Promise.resolve(null)
  }
  return Promise.resolve(null)
})

const nativeInvoke = vi.mocked(invoke)

vi.mock('@tauri-apps/api/core', () => ({
  invoke: vi.fn(),
}))

vi.mock('../mock-tauri', () => ({
  isTauri: () => false,
  mockInvoke: (cmd: string, args?: Record<string, unknown>) => mockInvokeFn(cmd, args),
}))

vi.mock('../lib/telemetry', () => ({
  trackEvent: trackEventMock,
}))

async function renderLoadedSettings(): Promise<Settings> {
  const { result } = renderHook(() => useSettings())

  await waitFor(() => {
    expect(result.current.loaded).toBe(true)
  })

  return result.current.settings
}

function changedSettings(): Settings {
  return {
    auto_pull_interval_minutes: null,
    git_enabled: null,
    git_path: null,
    git_provider: null,
    git_wsl_distro: null,
    autogit_enabled: false,
    autogit_idle_threshold_seconds: 120,
    autogit_inactive_threshold_seconds: 45,
    auto_advance_inbox_after_organize: false,
    telemetry_consent: null,
    crash_reporting_enabled: null,
    analytics_enabled: null,
    anonymous_id: null,
    release_channel: null,
    automatic_update_checks_enabled: false,
    theme_mode: null,
    ui_language: 'zh-CN',
    date_display_format: 'iso',
    note_width_mode: 'wide',
    sidebar_type_pluralization_enabled: false,
    ai_features_enabled: null,
    default_ai_agent: null,
    default_ai_target: null,
    ai_model_providers: null,
    ai_workspace_conversations: null,
    hide_gitignored_files: false,
    multi_workspace_enabled: null,
    all_notes_show_pdfs: true,
    all_notes_show_images: false,
    all_notes_show_unsupported: true,
  }
}

describe('useSettings', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    trackEventMock.mockClear()
    mockSettingsStore = { ...defaultSettings }
    nativeInvoke.mockResolvedValue(undefined)
  })

  it('returns empty settings initially', () => {
    mockInvokeFn.mockImplementationOnce(() => new Promise(() => {}))

    const { result, unmount } = renderHook(() => useSettings())
    expect(result.current.settings).toEqual(defaultSettings)
    expect(result.current.loaded).toBe(false)
    unmount()
  })

  it('loads settings from backend on mount', async () => {
    mockSettingsStore = { ...savedSettings }
    const { result } = renderHook(() => useSettings())

    await waitFor(() => {
      expect(result.current.loaded).toBe(true)
    })

    expect(result.current.settings.auto_pull_interval_minutes).toBe(15)
    expect(mockInvokeFn).toHaveBeenCalledWith('get_settings', {})
  })

  it('loads settings from native invoke when Tauri globals are not detectable', async () => {
    nativeInvoke.mockResolvedValueOnce({ ...savedSettings, ui_language: 'zh-Hans' })

    const settings = await renderLoadedSettings()

    expect(settings.ui_language).toBe('zh-CN')
    expect(mockInvokeFn).not.toHaveBeenCalledWith('get_settings', {})
  })

  it('normalizes a legacy beta release channel back to stable on load', async () => {
    mockSettingsStore = {
      ...savedSettings,
      release_channel: 'beta',
    }

    const settings = await renderLoadedSettings()
    expect(settings.release_channel).toBeNull()
  })

  it('normalizes unsupported language preferences on load', async () => {
    mockSettingsStore = {
      ...savedSettings,
      ui_language: 'xx-ZZ' as Settings['ui_language'],
    }

    const settings = await renderLoadedSettings()
    expect(settings.ui_language).toBeNull()
  })

  it('normalizes unsupported note width modes on load', async () => {
    mockSettingsStore = {
      ...savedSettings,
      note_width_mode: 'expanded' as Settings['note_width_mode'],
    }

    const settings = await renderLoadedSettings()
    expect(settings.note_width_mode).toBeNull()
  })

  it('normalizes unsupported date display formats on load', async () => {
    mockSettingsStore = {
      ...savedSettings,
      date_display_format: 'long' as Settings['date_display_format'],
    }

    const settings = await renderLoadedSettings()
    expect(settings.date_display_format).toBeNull()
  })

  it('drops malformed AI workspace conversation settings while preserving valid rows', async () => {
    mockSettingsStore = {
      ...savedSettings,
      ai_workspace_conversations: [
        { id: null, title: 'Broken chat', target_id: null },
        { id: '  thread-1  ', title: '  Research plan  ', target_id: null, archived: false },
        { id: 'thread-2', title: null, target_id: 'agent:codex' },
      ] as unknown as Settings['ai_workspace_conversations'],
    }

    const settings = await renderLoadedSettings()
    expect(settings.ai_workspace_conversations).toEqual([
      { archived: false, id: 'thread-1', target_id: null, title: 'Research plan' },
    ])
  })

  it('saves settings via backend', async () => {
    const { result } = renderHook(() => useSettings())

    await waitFor(() => {
      expect(result.current.loaded).toBe(true)
    })

    const newSettings = changedSettings()

    await act(async () => {
      await result.current.saveSettings(newSettings)
    })

    expect(mockInvokeFn).toHaveBeenCalledWith('save_settings', { settings: newSettings })
    expect(result.current.settings).toEqual(newSettings)
  })

  it('tracks theme mode changes after settings save succeeds', async () => {
    const { result } = renderHook(() => useSettings())

    await waitFor(() => {
      expect(result.current.loaded).toBe(true)
    })

    await act(async () => {
      await result.current.saveSettings({
        ...defaultSettings,
        theme_mode: 'system',
      })
    })

    expect(trackEventMock).toHaveBeenCalledWith('theme_mode_changed', { mode: 'system' })
  })

  it('preserves the Gitignored files visibility preference', async () => {
    mockSettingsStore = {
      ...savedSettings,
      hide_gitignored_files: false,
    }

    const settings = await renderLoadedSettings()

    expect(settings.hide_gitignored_files).toBe(false)
  })

  it('preserves All Notes file visibility preferences', async () => {
    mockSettingsStore = {
      ...savedSettings,
      all_notes_show_pdfs: true,
      all_notes_show_images: false,
      all_notes_show_unsupported: true,
    }

    const settings = await renderLoadedSettings()

    expect(settings.all_notes_show_pdfs).toBe(true)
    expect(settings.all_notes_show_images).toBe(false)
    expect(settings.all_notes_show_unsupported).toBe(true)
  })

  it('toggles Gitignored file visibility from the command event', async () => {
    const listener = vi.fn()
    window.addEventListener(GITIGNORED_VISIBILITY_CHANGED_EVENT, listener)
    const { result } = renderHook(() => useSettings())

    await waitFor(() => {
      expect(result.current.loaded).toBe(true)
    })

    await act(async () => {
      window.dispatchEvent(new CustomEvent(TOGGLE_GITIGNORED_VISIBILITY_EVENT))
    })

    await waitFor(() => {
      expect(result.current.settings.hide_gitignored_files).toBe(false)
    })
    expect(listener).toHaveBeenCalledTimes(1)
    window.removeEventListener(GITIGNORED_VISIBILITY_CHANGED_EVENT, listener)
  })

  it('saves settings through native invoke when Tauri globals are not detectable', async () => {
    const { result } = renderHook(() => useSettings())

    await waitFor(() => {
      expect(result.current.loaded).toBe(true)
    })

    const newSettings = changedSettings()

    vi.clearAllMocks()
    nativeInvoke.mockResolvedValueOnce(null)

    await act(async () => {
      await result.current.saveSettings(newSettings)
    })

    expect(nativeInvoke).toHaveBeenCalledWith('save_settings', { settings: newSettings })
    expect(mockInvokeFn).not.toHaveBeenCalled()
    expect(result.current.settings).toEqual(newSettings)
  })

  it('handles load error gracefully', async () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
    mockInvokeFn.mockImplementationOnce(() => Promise.reject(new Error('no config')))

    const { result } = renderHook(() => useSettings())

    await waitFor(() => {
      expect(result.current.loaded).toBe(true)
    })

    // Should fall back to empty settings
    expect(result.current.settings).toEqual(defaultSettings)
    warnSpy.mockRestore()
  })

  it('handles save error gracefully', async () => {
    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
    const { result } = renderHook(() => useSettings())

    await waitFor(() => {
      expect(result.current.loaded).toBe(true)
    })

    mockInvokeFn.mockImplementationOnce(() => Promise.reject(new Error('write failed')))

    await act(async () => {
      await result.current.saveSettings(savedSettings)
    })

    // Settings should not have changed on error
    expect(result.current.settings).toEqual(defaultSettings)
    errorSpy.mockRestore()
  })
})
