import { expect, type Page } from '@playwright/test'
import fs from 'fs'
import os from 'os'
import path from 'path'
import type { FolderNode } from '../../src/types'
import { installFixtureVaultDesktopBridgeInBrowser } from './fixtureVaultDesktopBridge'

const FIXTURE_VAULT = path.resolve('tests/fixtures/test-vault')
const FIXTURE_VAULT_READY_TIMEOUT = 30_000
const FIXTURE_VAULT_REMOVE_RETRIES = 10
const FIXTURE_VAULT_REMOVE_RETRY_DELAY_MS = 100
const CLAUDE_CODE_ONBOARDING_DISMISSED_KEY = 'tolaria:claude-code-onboarding-dismissed'
type FixtureCommandArgs = Record<string, unknown> | undefined

interface FixtureVaultPageArgs {
  page: Page
  vaultPath: string
  isGitRepo: boolean
  folders: FolderNode[]
  installedAiAgents: string[]
}

interface FixturePageArgs {
  page: Page
}

interface FixtureVaultOptions {
  isGitRepo?: boolean
  expectedReadyTitle?: string
  folders?: FolderNode[]
  installedAiAgents?: string[]
}

interface CopyDirArgs {
  src: string
  dest: string
}

interface RemoveFixtureVaultArgs {
  tempVaultDir: string
}

function copyDirSync({ src, dest }: CopyDirArgs): void {
  fs.mkdirSync(dest, { recursive: true })
  for (const item of fs.readdirSync(src, { withFileTypes: true })) {
    const sourcePath = path.join(src, item.name)
    const destinationPath = path.join(dest, item.name)
    if (item.isDirectory()) {
      copyDirSync({ src: sourcePath, dest: destinationPath })
      continue
    }
    fs.copyFileSync(sourcePath, destinationPath)
  }
}

export function createFixtureVaultCopy(): string {
  const tempVaultDir = fs.mkdtempSync(path.join(os.tmpdir(), 'laputa-test-vault-'))
  copyDirSync({ src: FIXTURE_VAULT, dest: tempVaultDir })
  return tempVaultDir
}

function removeFixtureVaultDirectory({ tempVaultDir }: RemoveFixtureVaultArgs): void {
  fs.rmSync(tempVaultDir, {
    recursive: true,
    force: true,
    maxRetries: FIXTURE_VAULT_REMOVE_RETRIES,
    retryDelay: FIXTURE_VAULT_REMOVE_RETRY_DELAY_MS,
  })
}

export function removeFixtureVaultCopy(tempVaultDir: string | null | undefined): void {
  if (!tempVaultDir) return
  removeFixtureVaultDirectory({ tempVaultDir })
}

async function installFixtureVaultInitScript({ page, vaultPath, isGitRepo, folders, installedAiAgents }: FixtureVaultPageArgs): Promise<void> {
  await page.addInitScript(({ dismissedKey, fixtureFolders, fixtureInstalledAiAgents, initialIsGitRepo, resolvedVaultPath }: { dismissedKey: string; fixtureFolders: FolderNode[]; fixtureInstalledAiAgents: string[]; initialIsGitRepo: boolean; resolvedVaultPath: string }) => {
    localStorage.clear()
    localStorage.setItem(dismissedKey, '1')
    let gitRepoReady = initialIsGitRepo
    const installedAiAgentSet = new Set(fixtureInstalledAiAgents)

    const jsonHeaders = { 'Content-Type': 'application/json' }
    const FRONTMATTER_DELIMITER = '---'
    const DEFAULT_FRONTMATTER_LINE_ENDING = '\n'
    const nativeFetch = window.fetch.bind(window)

    window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
      const requestUrl = typeof input === 'string'
        ? input
        : input instanceof Request
          ? input.url
          : input.toString()

      if (requestUrl.endsWith('/api/vault/ping') || requestUrl.includes('/api/vault/ping?')) {
        return Promise.resolve(new Response(JSON.stringify({ ok: true }), {
          status: 200,
          headers: { 'Content-Type': 'application/json' },
        }))
      }

      return nativeFetch(input, init)
    }

    const readJson = async (url: string, init?: RequestInit) => {
      const response = await nativeFetch(url, init)
      if (!response.ok) {
        let message = `HTTP ${response.status}`
        try {
          const body = await response.json() as { error?: string }
          message = body.error ?? message
        } catch {
          // Preserve the HTTP status fallback when the body is not JSON.
        }
        throw new Error(message)
      }
      return response.json()
    }

    const splitFrontmatter = (content: string) => {
      const lineEnding = content.startsWith(`${FRONTMATTER_DELIMITER}\r\n`)
        ? '\r\n'
        : content.startsWith(`${FRONTMATTER_DELIMITER}\n`)
          ? '\n'
          : null
      if (!lineEnding) {
        return { frontmatter: null as string | null, body: content, lineEnding: DEFAULT_FRONTMATTER_LINE_ENDING }
      }

      const afterOpen = content.slice(FRONTMATTER_DELIMITER.length + lineEnding.length)
      if (afterOpen.startsWith(FRONTMATTER_DELIMITER)) {
        return { frontmatter: '', body: afterOpen.slice(FRONTMATTER_DELIMITER.length), lineEnding }
      }

      const closeMarker = `${lineEnding}${FRONTMATTER_DELIMITER}`
      const closeIndex = afterOpen.indexOf(closeMarker)
      if (closeIndex === -1) {
        return { frontmatter: null as string | null, body: content, lineEnding: DEFAULT_FRONTMATTER_LINE_ENDING }
      }

      return {
        frontmatter: afterOpen.slice(0, closeIndex),
        body: afterOpen.slice(closeIndex + closeMarker.length),
        lineEnding,
      }
    }

    const splitFrontmatterEntries = (frontmatter: string) => {
      const lines = frontmatter.split(/\r?\n/)
      const entries: Array<{ key: string; lines: string[] }> = []
      let current: { key: string; lines: string[] } | null = null

      for (const line of lines) {
        const match = line.match(/^([^:\n]+):(.*)$/)
        if (match && !line.startsWith(' ')) {
          if (current) entries.push(current)
          current = { key: match[1].trim(), lines: [line] }
          continue
        }

        if (current) {
          current.lines.push(line)
        } else if (line.trim() !== '') {
          current = { key: '', lines: [line] }
        }
      }

      if (current) entries.push(current)
      return entries
    }

    const FRONTMATTER_ALIAS_GROUPS = {
      aliases: ['aliases'],
      color: ['color'],
      status: ['status'],
      template: ['template'],
      title: ['title'],
      type: ['type', 'is_a', 'is a'],
      view: ['view'],
      visible: ['visible'],
      _archived: ['_archived', 'archived'],
      _favorite: ['_favorite'],
      _favorite_index: ['_favorite_index'],
      _icon: ['_icon', 'icon'],
      _list_properties_display: ['_list_properties_display'],
      _organized: ['_organized'],
      _order: ['_order', 'order'],
      _sidebar_label: ['_sidebar_label', 'sidebar_label', 'sidebar label'],
      _sort: ['_sort', 'sort'],
      _width: ['_width', 'width'],
      belongs_to: ['belongs_to', 'belongs to'],
      related_to: ['related_to', 'related to'],
    } as const
    const canonicalFrontmatterAliases = new Map<string, string>()
    const canonicalWriteKeys = new Set([
      'type',
      '_archived',
      '_favorite',
      '_favorite_index',
      '_icon',
      '_list_properties_display',
      '_organized',
      '_order',
      '_sidebar_label',
      '_sort',
      '_width',
    ])

    for (const [canonicalKey, aliases] of Object.entries(FRONTMATTER_ALIAS_GROUPS)) {
      for (const alias of aliases) {
        canonicalFrontmatterAliases.set(alias, canonicalKey)
      }
    }

    const normalizeFrontmatterKey = (key: string) => key
      .trim()
      .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
      .toLowerCase()
      .replace(/\s+/g, '_')

    const canonicalFrontmatterKey = (key: string) =>
      canonicalFrontmatterAliases.get(normalizeFrontmatterKey(key)) ?? normalizeFrontmatterKey(key)

    const canonicalFrontmatterWriteKey = (key: string) => {
      const canonicalKey = canonicalFrontmatterKey(key)
      return canonicalWriteKeys.has(canonicalKey) ? canonicalKey : key
    }

    const frontmatterKeysMatch = (left: string, right: string) =>
      canonicalFrontmatterKey(left) === canonicalFrontmatterKey(right)

    const serializeFrontmatterValue = (value: unknown): string[] => {
      if (Array.isArray(value)) {
        if (value.length === 0) return ['[]']
        return [''].concat(value.map((item) => `  - ${JSON.stringify(String(item))}`))
      }
      if (typeof value === 'boolean' || typeof value === 'number') {
        return [String(value)]
      }
      return [JSON.stringify(String(value ?? ''))]
    }

    const replaceFrontmatterEntry = (content: string, key: string, value: unknown) => {
      const { frontmatter, body, lineEnding } = splitFrontmatter(content)
      const writeKey = canonicalFrontmatterWriteKey(key)
      const entryLines = serializeFrontmatterValue(value)
      const nextEntryLines =
        entryLines[0] === ''
          ? [`${writeKey}:`, ...entryLines.slice(1)]
          : [`${writeKey}: ${entryLines[0]}`]

      if (frontmatter === null) {
        return `${FRONTMATTER_DELIMITER}\n${nextEntryLines.join('\n')}\n${FRONTMATTER_DELIMITER}\n${body}`
      }

      const nextEntries = splitFrontmatterEntries(frontmatter)
        .filter((entry) => entry.key !== '')
        .map((entry) => (frontmatterKeysMatch(entry.key, key) ? { key: writeKey, lines: nextEntryLines } : entry))

      const hasEntry = nextEntries.some((entry) => frontmatterKeysMatch(entry.key, key))
      if (!hasEntry) {
        nextEntries.push({ key: writeKey, lines: nextEntryLines })
      }

      return `${FRONTMATTER_DELIMITER}${lineEnding}${nextEntries.flatMap((entry) => entry.lines).join(lineEnding)}${lineEnding}${FRONTMATTER_DELIMITER}${body}`
    }

    const removeFrontmatterEntry = (content: string, key: string) => {
      const { frontmatter, body, lineEnding } = splitFrontmatter(content)
      if (frontmatter === null) return content

      const nextEntries = splitFrontmatterEntries(frontmatter)
        .filter((entry) => entry.key !== '' && !frontmatterKeysMatch(entry.key, key))

      if (nextEntries.length === 0) {
        return body
      }

      return `${FRONTMATTER_DELIMITER}${lineEnding}${nextEntries.flatMap((entry) => entry.lines).join(lineEnding)}${lineEnding}${FRONTMATTER_DELIMITER}${body}`
    }

    const persistFrontmatterChange = async (notePath: string, transform: (content: string) => string) => {
      const current = await readJson(
        `/api/vault/content?path=${encodeURIComponent(notePath)}`,
      ) as { content: string }
      const updatedContent = transform(current.content)
      await readJson('/api/vault/save', {
        method: 'POST',
        headers: jsonHeaders,
        body: JSON.stringify({ path: notePath, content: updatedContent }),
      })
      return updatedContent
    }

    const activeVaultList = {
      vaults: [{ label: 'Test Vault', path: resolvedVaultPath }],
      active_vault: resolvedVaultPath,
      hidden_defaults: [],
    }

    const readVaultList = (commandArgs?: Record<string, unknown>, reload = false) => {
      const resolvedPath = String(commandArgs?.path ?? resolvedVaultPath)
      return readJson(
        `/api/vault/list?path=${encodeURIComponent(resolvedPath)}&reload=${reload ? '1' : '0'}`,
      )
    }

    const renameNoteRequest = (payload: Record<string, unknown>) =>
      readJson('/api/vault/rename', {
        method: 'POST',
        headers: jsonHeaders,
        body: JSON.stringify(payload),
      })

    const readNestedCommandArgs = (commandArgs: FixtureCommandArgs) => {
      const nestedArgs = commandArgs?.args
      return nestedArgs && typeof nestedArgs === 'object'
        ? nestedArgs as Record<string, unknown>
        : null
    }

    const readCommandValue = (commandArgs: FixtureCommandArgs, key: string, fallback?: unknown) => {
      const directValue = commandArgs?.[key]
      if (directValue !== undefined) return directValue
      const nestedValue = readNestedCommandArgs(commandArgs)?.[key]
      return nestedValue ?? fallback
    }

    const readCommandString = (commandArgs: FixtureCommandArgs, key: string, fallback = '') =>
      String(readCommandValue(commandArgs, key, fallback))

    const buildFixtureStateHandlers = () => ({
      load_vault_list: () => activeVaultList,
      check_vault_exists: (commandArgs?: FixtureCommandArgs) =>
        readCommandString(commandArgs, 'path') === resolvedVaultPath,
      is_git_repo: () => gitRepoReady,
      init_git_repo: () => {
        gitRepoReady = true
        return null
      },
      get_last_vault_path: () => resolvedVaultPath,
      get_default_vault_path: () => resolvedVaultPath,
      save_vault_list: () => null,
      save_settings: () => null,
      register_mcp_tools: () => null,
      get_mcp_config_snippet: () => JSON.stringify({
        mcpServers: {
          tolaria: {
            type: 'stdio',
            command: 'node',
            args: ['/fixture/Tolaria/mcp-server/index.js'],
            env: {
              WS_UI_PORT: '9711',
            },
          },
        },
      }, null, 2),
      reinit_telemetry: () => null,
      update_menu_state: () => null,
      get_settings: () => ({
        auto_pull_interval_minutes: 5,
        telemetry_consent: false,
        crash_reporting_enabled: null,
        analytics_enabled: null,
        anonymous_id: null,
        release_channel: null,
      }),
      get_ai_agents_status: () => {
        const availability = (agentId: string) => ({
          installed: installedAiAgentSet.has(agentId),
          version: installedAiAgentSet.has(agentId) ? 'mock' : null,
        })
        return {
          claude_code: availability('claude_code'),
          codex: availability('codex'),
          copilot: availability('copilot'),
          opencode: availability('opencode'),
          pi: availability('pi'),
          antigravity: availability('antigravity'),
          kiro: availability('kiro'),
          hermes: availability('hermes'),
        }
      },
    })

    const buildFixtureReadHandlers = () => ({
      list_vault: (commandArgs?: FixtureCommandArgs) => readVaultList(commandArgs),
      reload_vault: (commandArgs?: FixtureCommandArgs) => readVaultList(commandArgs, true),
      list_vault_folders: () => fixtureFolders,
      list_views: () => [],
      get_modified_files: () => [],
      detect_renames: () => [],
      reload_vault_entry: (commandArgs?: FixtureCommandArgs) =>
        readJson(`/api/vault/entry?path=${encodeURIComponent(readCommandString(commandArgs, 'path'))}`),
      get_note_content: async (commandArgs?: FixtureCommandArgs) => {
        const data = await readJson(
          `/api/vault/content?path=${encodeURIComponent(readCommandString(commandArgs, 'path'))}`,
        ) as { content: string }
        return data.content
      },
      validate_note_content: async (commandArgs?: FixtureCommandArgs) => {
        const data = await readJson(
          `/api/vault/content?path=${encodeURIComponent(readCommandString(commandArgs, 'path'))}`,
        ) as { content: string }
        return data.content === readCommandString(commandArgs, 'content')
      },
      get_all_content: (commandArgs?: FixtureCommandArgs) =>
        readJson(
          `/api/vault/all-content?path=${encodeURIComponent(readCommandString(commandArgs, 'path', resolvedVaultPath))}`,
        ),
      search_vault: (commandArgs?: FixtureCommandArgs) => {
        const resolvedPath = readCommandString(
          commandArgs,
          'path',
          readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
        )
        const query = encodeURIComponent(readCommandString(commandArgs, 'query'))
        const mode = encodeURIComponent(readCommandString(commandArgs, 'mode', 'all'))
        const excludeFrontmatter = readCommandValue(commandArgs, 'excludeFrontmatter') === true
          ? '&exclude_frontmatter=1'
          : ''
        return readJson(
          `/api/vault/search?vault_path=${encodeURIComponent(resolvedPath)}&query=${query}&mode=${mode}${excludeFrontmatter}`,
        )
      },
    })

    const buildFixtureWriteHandlers = () => ({
      save_note_content: (commandArgs?: FixtureCommandArgs) =>
        readJson('/api/vault/save', {
          method: 'POST',
          headers: jsonHeaders,
          body: JSON.stringify({
            path: readCommandValue(commandArgs, 'path'),
            content: readCommandValue(commandArgs, 'content'),
          }),
        }),
      create_note_content: async (commandArgs?: FixtureCommandArgs) => {
        const notePath = readCommandString(commandArgs, 'path')
        const existing = await nativeFetch(`/api/vault/content?path=${encodeURIComponent(notePath)}`)
        if (existing.ok) throw new Error(`File already exists: ${notePath}`)
        return readJson('/api/vault/save', {
          method: 'POST',
          headers: jsonHeaders,
          body: JSON.stringify({
            path: notePath,
            content: readCommandValue(commandArgs, 'content'),
          }),
        })
      },
      update_frontmatter: (commandArgs?: FixtureCommandArgs) =>
        persistFrontmatterChange(readCommandString(commandArgs, 'path'), (content) =>
          replaceFrontmatterEntry(
            content,
            readCommandString(commandArgs, 'key'),
            readCommandValue(commandArgs, 'value'),
          ),
        ),
      delete_frontmatter_property: (commandArgs?: FixtureCommandArgs) =>
        persistFrontmatterChange(
          readCommandString(commandArgs, 'path'),
          (content) => removeFrontmatterEntry(content, readCommandString(commandArgs, 'key')),
        ),
      rename_note: (commandArgs?: FixtureCommandArgs) =>
        renameNoteRequest({
          vault_path: readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
          old_path: readCommandValue(commandArgs, 'oldPath'),
          new_title: readCommandValue(commandArgs, 'newTitle'),
          old_title: readCommandValue(commandArgs, 'oldTitle', null),
        }),
      rename_note_filename: (commandArgs?: FixtureCommandArgs) =>
        readJson('/api/vault/rename-filename', {
          method: 'POST',
          headers: jsonHeaders,
          body: JSON.stringify({
            vault_path: readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
            old_path: readCommandValue(commandArgs, 'oldPath'),
            new_filename_stem: readCommandValue(commandArgs, 'newFilenameStem'),
          }),
        }),
      auto_rename_untitled: async (commandArgs?: FixtureCommandArgs) => {
        const notePath = readCommandString(commandArgs, 'notePath')
        const contentData = await readJson(
          `/api/vault/content?path=${encodeURIComponent(notePath)}`,
        ) as { content: string }
        const match = contentData.content.match(/^#\s+(.+)$/m)
        if (!match) return null
        return renameNoteRequest({
          vault_path: readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
          old_path: notePath,
          new_title: match[1].trim(),
        })
      },
    })

    const applyFixtureVaultOverrides = (
      handlers: Record<string, ((args?: unknown) => unknown)> | null | undefined,
    ) => {
      if (!handlers) return handlers
      Object.assign(
        handlers,
        buildFixtureStateHandlers(),
        buildFixtureReadHandlers(),
        buildFixtureWriteHandlers(),
      )
      return handlers
    }

    let ref = applyFixtureVaultOverrides(
      (window.__mockHandlers as Record<string, ((args?: unknown) => unknown)> | undefined),
    ) ?? null

    Object.defineProperty(window, '__mockHandlers', {
      configurable: true,
      set(value) {
        ref = applyFixtureVaultOverrides(
          value as Record<string, ((args?: unknown) => unknown)> | undefined,
        ) ?? null
      },
      get() {
        return applyFixtureVaultOverrides(ref) ?? ref
      },
    })
  }, {
    dismissedKey: CLAUDE_CODE_ONBOARDING_DISMISSED_KEY,
    fixtureFolders: folders,
    fixtureInstalledAiAgents: installedAiAgents,
    initialIsGitRepo: isGitRepo,
    resolvedVaultPath: vaultPath,
  })
}

async function waitForFixtureVaultReady({ page, expectedTitle }: FixturePageArgs & { expectedTitle: string }): Promise<void> {
  await page.goto('/', { waitUntil: 'domcontentloaded' })
  await page.waitForFunction(() => Boolean(window.__mockHandlers?.list_vault))
  await page.locator('[data-testid="note-list-container"]').waitFor({ timeout: FIXTURE_VAULT_READY_TIMEOUT })
  await expect(page.getByText(expectedTitle, { exact: true }).first()).toBeVisible({
    timeout: FIXTURE_VAULT_READY_TIMEOUT,
  })
}

export async function openFixtureVault(
  page: Page,
  vaultPath: string,
  options: FixtureVaultOptions = {},
): Promise<void> {
  await installFixtureVaultInitScript({
    page,
    vaultPath,
    isGitRepo: options.isGitRepo ?? true,
    folders: options.folders ?? [],
    installedAiAgents: options.installedAiAgents ?? [],
  })
  await waitForFixtureVaultReady({
    page,
    expectedTitle: options.expectedReadyTitle ?? 'Alpha Project',
  })
}

async function installFixtureVaultDesktopBridge({ page }: FixturePageArgs): Promise<void> {
  await page.evaluate(installFixtureVaultDesktopBridgeInBrowser)

  await page.waitForFunction(() => Boolean(window.__TAURI_INTERNALS__))
}

/**
 * Browser harness for desktop command-routing tests.
 *
 * This stubs the Tauri invoke bridge inside Playwright so tests can exercise
 * renderer shortcut dispatch and desktop menu-command dispatch without a native
 * shell. It is deterministic, but it is not a substitute for real native QA.
 */
export async function openFixtureVaultDesktopHarness(
  page: Page,
  vaultPath: string,
  options: FixtureVaultOptions = {},
): Promise<void> {
  await openFixtureVault(page, vaultPath, options)
  await installFixtureVaultDesktopBridge({ page })
}

export const openFixtureVaultTauri = openFixtureVaultDesktopHarness
