import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { existsSync, realpathSync } from 'node:fs'
import {
  chmod,
  mkdir,
  mkdtemp,
  readFile,
  symlink,
  writeFile,
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import process from 'node:process'
import test from 'node:test'

import {
  BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
  FIXED_LINUXDEPLOY_APPRUN_DIR_LINE,
  REAL_APPIMAGE_PLUGIN_NAME,
  appImagePluginWrapperSource,
  patchAppRunText,
  preparePluginWrapper,
} from './appimage-launcher-tools.mjs'

function brokenResolverDir(invokedPath) {
  return realpathSync(dirname(invokedPath))
}

function fixedResolverDir(invokedPath) {
  return dirname(realpathSync(invokedPath))
}

test('patches linuxdeploy AppRun wrapper to resolve the invoked path before dirname', () => {
  const original = [
    '#! /usr/bin/env bash',
    '# autogenerated by linuxdeploy',
    BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
    'exec "$this_dir"/AppRun.wrapped "$@"',
  ].join('\n')

  const patched = patchAppRunText(original)

  assert.equal(patched.changed, true)
  assert.equal(patched.text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false)
  assert.equal(patched.text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true)
})

test('fixed resolver follows absolute and relative symlinks before choosing AppDir', async () => {
  const root = await mkdtemp(join(tmpdir(), 'tolaria-apprun-resolver-'))
  const appDir = join(root, 'Tolaria.AppDir')
  const binDir = join(root, 'bin')
  const relativeDir = join(root, 'relative-bin')
  const appRun = join(appDir, 'AppRun')

  await mkdir(appDir)
  await mkdir(binDir)
  await mkdir(relativeDir)
  await writeFile(appRun, '#! /usr/bin/env bash\n', 'utf8')

  const absoluteSymlink = join(binDir, 'tolaria')
  const relativeSymlink = join(relativeDir, 'tolaria')

  await symlink(appRun, absoluteSymlink)
  await symlink(`../${basename(appDir)}/AppRun`, relativeSymlink)

  assert.equal(brokenResolverDir(absoluteSymlink), realpathSync(binDir))
  assert.equal(fixedResolverDir(absoluteSymlink), realpathSync(appDir))
  assert.equal(brokenResolverDir(relativeSymlink), realpathSync(relativeDir))
  assert.equal(fixedResolverDir(relativeSymlink), realpathSync(appDir))
})

test('plugin wrapper patches AppRun before delegating to the real output plugin', async () => {
  const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-plugin-'))
  const appDir = join(root, 'Tolaria.AppDir')
  const appRun = join(appDir, 'AppRun')
  const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage')
  const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage')
  const pluginMarker = join(root, 'plugin-ran')

  await mkdir(appDir)
  await writeFile(
    appRun,
    [
      '#! /usr/bin/env bash',
      '# autogenerated by linuxdeploy',
      BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
      'exec "$this_dir"/AppRun.wrapped "$@"',
    ].join('\n'),
    'utf8',
  )
  await writeFile(wrapper, appImagePluginWrapperSource(), 'utf8')
  await chmod(wrapper, 0o755)
  await writeFile(
    realPlugin,
    `#!/usr/bin/env bash\nset -euo pipefail\ntouch "${pluginMarker}"\n`,
    'utf8',
  )
  await chmod(realPlugin, 0o755)

  const result = spawnSync(wrapper, [], {
    encoding: 'utf8',
    env: {
      ...process.env,
      APPDIR: appDir,
      TOLARIA_APPIMAGE_REAL_PLUGIN: realPlugin,
    },
  })

  assert.equal(result.status, 0, result.stderr)
  assert.equal(existsSync(pluginMarker), true)

  const patched = await readFile(appRun, 'utf8')
  assert.equal(patched.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false)
  assert.equal(patched.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true)
})

test('plugin wrapper keeps the delegated appimage plugin basename canonical', async () => {
  const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-tools-'))
  const { realPluginPath, wrapperPath } = await preparePluginWrapper({
    toolsDir: root,
  })

  assert.equal(basename(wrapperPath), 'linuxdeploy-plugin-appimage.AppImage')
  assert.equal(
    realPluginPath,
    join(root, 'tolaria-real-linuxdeploy-plugin-appimage', 'linuxdeploy-plugin-appimage.AppImage'),
  )
  assert.equal(REAL_APPIMAGE_PLUGIN_NAME.endsWith('/linuxdeploy-plugin-appimage.AppImage'), true)

  const wrapper = await readFile(wrapperPath, 'utf8')
  assert.equal(wrapper.includes('linuxdeploy-plugin-appimage.real.AppImage'), false)
})

test('plugin wrapper bundles fcitx GTK3 input module before sealing AppImage', async () => {
  const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-fcitx-'))
  const appDir = join(root, 'Tolaria.AppDir')
  const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage')
  const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage')
  const pluginMarker = join(root, 'plugin-ran')
  const hostModule = join(root, 'host', 'im-fcitx5.so')
  const hostLibraryDir = join(root, 'host-lib')
  const hostLibrary = join(hostLibraryDir, 'libFcitx5GClient.so.2')

  await mkdir(appDir)
  await mkdir(dirname(hostModule), { recursive: true })
  await mkdir(hostLibraryDir)
  await writeFile(hostModule, 'fake fcitx gtk module', 'utf8')
  await writeFile(hostLibrary, 'fake fcitx client library', 'utf8')
  await writeFile(wrapper, appImagePluginWrapperSource(), 'utf8')
  await chmod(wrapper, 0o755)
  await writeFile(
    realPlugin,
    `#!/usr/bin/env bash\nset -euo pipefail\ntouch "${pluginMarker}"\n`,
    'utf8',
  )
  await chmod(realPlugin, 0o755)

  const result = spawnSync(wrapper, [], {
    encoding: 'utf8',
    env: {
      ...process.env,
      APPDIR: appDir,
      TOLARIA_APPIMAGE_REAL_PLUGIN: realPlugin,
      TOLARIA_FCITX_GTK3_IM_MODULE: hostModule,
      TOLARIA_FCITX_LIBRARY_DIR: hostLibraryDir,
    },
  })

  assert.equal(result.status, 0, result.stderr)
  assert.equal(existsSync(pluginMarker), true)
  assert.equal(
    existsSync(
      join(
        appDir,
        'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so',
      ),
    ),
    true,
  )
  assert.equal(existsSync(join(appDir, 'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2')), true)
})
