import test from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { buildProtectedAppImageUpdateCommand, updateAppImageFromRelease } from './appimage-updater';

const appImageBytes = Buffer.from('appimage');
const appImageHash = createHash('sha256').update(appImageBytes).digest('hex');

test('updateAppImageFromRelease verifies hash and atomically replaces writable AppImage', async () => {
  const writes: Array<{ path: string; data: Buffer }> = [];
  const chmods: Array<{ path: string; mode: number }> = [];
  const renames: Array<{ from: string; to: string }> = [];

  const result = await updateAppImageFromRelease({
    release: {
      tag_name: 'v0.15.0',
      prerelease: false,
      draft: false,
      assets: [{ name: 'SubMiner.AppImage', browser_download_url: 'https://example.test/app' }],
    },
    sha256Sums: new Map([['SubMiner.AppImage', appImageHash]]),
    appImagePath: '/home/kyle/.local/bin/SubMiner.AppImage',
    downloadAsset: async () => appImageBytes,
    fs: {
      stat: async () => ({
        isFile: () => true,
        mode: 0o755,
      }),
      access: async () => {},
      writeFile: async (targetPath, data) => {
        writes.push({ path: targetPath, data });
      },
      chmod: async (targetPath, mode) => {
        chmods.push({ path: targetPath, mode });
      },
      rename: async (from, to) => {
        renames.push({ from, to });
      },
      unlink: async () => {},
    },
  });

  assert.deepEqual(result, {
    status: 'updated',
    path: '/home/kyle/.local/bin/SubMiner.AppImage',
  });
  assert.deepEqual(writes, [
    { path: '/home/kyle/.local/bin/.SubMiner.AppImage.update', data: appImageBytes },
  ]);
  assert.deepEqual(chmods, [
    { path: '/home/kyle/.local/bin/.SubMiner.AppImage.update', mode: 0o755 },
  ]);
  assert.deepEqual(renames, [
    {
      from: '/home/kyle/.local/bin/.SubMiner.AppImage.update',
      to: '/home/kyle/.local/bin/SubMiner.AppImage',
    },
  ]);
});

test('updateAppImageFromRelease reports protected command without replacing non-writable AppImage', async () => {
  const result = await updateAppImageFromRelease({
    release: {
      tag_name: 'v0.15.0',
      prerelease: false,
      draft: false,
      assets: [{ name: 'SubMiner.AppImage', browser_download_url: 'https://example.test/app' }],
    },
    sha256Sums: new Map([['SubMiner.AppImage', appImageHash]]),
    appImagePath: '/opt/SubMiner/SubMiner.AppImage',
    downloadAsset: async () => appImageBytes,
    fs: {
      stat: async () => ({
        isFile: () => true,
        mode: 0o755,
      }),
      access: async () => {
        throw new Error('EACCES');
      },
      writeFile: async () => {
        throw new Error('unexpected write');
      },
      chmod: async () => {},
      rename: async () => {},
      unlink: async () => {},
    },
  });

  assert.equal(result.status, 'protected');
  assert.equal(result.path, '/opt/SubMiner/SubMiner.AppImage');
  assert.match(result.command ?? '', /curl -fSL 'https:\/\/example\.test\/app' -o "\$tmp"/);
  assert.match(result.command ?? '', /sha256sum -c -/);
  assert.match(result.command ?? '', /sudo mv "\$tmp" '\/opt\/SubMiner\/SubMiner\.AppImage'/);
});

test('buildProtectedAppImageUpdateCommand quotes inputs and verifies checksum before sudo move', () => {
  const command = buildProtectedAppImageUpdateCommand(
    "https://example.test/Sub Miner.AppImage?sig='abc'",
    "/opt/Sub Miner/SubMiner's.AppImage",
    'ABCDEF',
  );

  assert.match(command, /trap 'rm -f "\$tmp"' EXIT/);
  assert.match(
    command,
    /curl -fSL 'https:\/\/example\.test\/Sub Miner\.AppImage\?sig='\\''abc'\\''' -o "\$tmp"/,
  );
  assert.match(command, /printf '%s  %s\\n' 'abcdef' "\$tmp" \| sha256sum -c -/);
  assert.match(command, /sudo mv "\$tmp" '\/opt\/Sub Miner\/SubMiner'\\''s\.AppImage'/);
  assert.match(command, /sudo chmod \+x '\/opt\/Sub Miner\/SubMiner'\\''s\.AppImage'/);
});

test('updateAppImageFromRelease aborts on hash mismatch', async () => {
  const result = await updateAppImageFromRelease({
    release: {
      tag_name: 'v0.15.0',
      prerelease: false,
      draft: false,
      assets: [{ name: 'SubMiner.AppImage', browser_download_url: 'https://example.test/app' }],
    },
    sha256Sums: new Map([['SubMiner.AppImage', '0'.repeat(64)]]),
    appImagePath: '/home/kyle/.local/bin/SubMiner.AppImage',
    downloadAsset: async () => appImageBytes,
    fs: {
      stat: async () => ({
        isFile: () => true,
        mode: 0o755,
      }),
      access: async () => {},
      writeFile: async () => {
        throw new Error('unexpected write');
      },
      chmod: async () => {},
      rename: async () => {},
      unlink: async () => {},
    },
  });

  assert.equal(result.status, 'hash-mismatch');
});
