// Plan §3.A6 / spec §7.2 — github + https tarball install backend.
//
// We don't reach the network in tests; an in-memory `fetcher` returns the
// gzipped bytes we just wrote with `tar.create()`. The test validates:
//   1. github:owner/repo source resolves to the codeload URL pattern and
//      extracts cleanly into the registry under the manifest's id.
//   2. https://…tar.gz source extracts identically and records
//      sourceKind='url'.
//   3. Size cap rejection blocks a tarball that exceeds maxBytes.
//   4. Symlink entries inside an archive are rejected.

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import url from 'node:url';
import { createHash } from 'node:crypto';
import { Readable } from 'node:stream';
import { mkdtemp, rm, writeFile, mkdir, symlink, readdir, readFile } from 'node:fs/promises';
import Database from 'better-sqlite3';
import { c as tarCreate } from 'tar';
import { migratePlugins } from '../src/plugins/persistence.js';
import { installPlugin, type ArchiveFetcher } from '../src/plugins/installer.js';

const __dirname = path.dirname(url.fileURLToPath(import.meta.url));

let db: Database.Database;
let tmpRoot: string;
let pluginsRoot: string;

async function buildFixtureTarball(args: {
  rootPrefix: string;
  pluginSubpath?: string;
  withSymlink?: boolean;
  bigPaddingBytes?: number;
}): Promise<Buffer> {
  // Write the fixture into a temp folder that mirrors the tar layout
  // codeload uses: `<repo>-<sha>/<files>`.
  const tmp = await mkdtemp(path.join(os.tmpdir(), 'od-fixture-'));
  const wrapper = path.join(tmp, args.rootPrefix);
  const pluginRoot = args.pluginSubpath
    ? path.join(wrapper, args.pluginSubpath)
    : wrapper;
  await mkdir(pluginRoot, { recursive: true });
  const fixtureSrc = path.join(__dirname, 'fixtures', 'plugin-fixtures', 'sample-plugin');
  for (const entry of await readdir(fixtureSrc)) {
    const data = await fs.promises.readFile(path.join(fixtureSrc, entry));
    await writeFile(path.join(pluginRoot, entry), data);
  }
  if (args.withSymlink) {
    await symlink('SKILL.md', path.join(pluginRoot, 'symlink-here'));
  }
  if (args.bigPaddingBytes) {
    const buf = Buffer.alloc(args.bigPaddingBytes, 0);
    await writeFile(path.join(pluginRoot, 'huge.bin'), buf);
  }
  const stream = tarCreate(
    { cwd: tmp, gzip: true },
    [args.rootPrefix],
  ) as unknown as NodeJS.ReadableStream;
  const chunks: Buffer[] = [];
  for await (const chunk of stream) chunks.push(Buffer.from(chunk as Buffer));
  await rm(tmp, { recursive: true, force: true });
  return Buffer.concat(chunks);
}

function makeFetcher(buf: Buffer): ArchiveFetcher {
  return async () => ({
    ok: true,
    status: 200,
    statusText: 'OK',
    body: Readable.from([buf]),
  });
}

function makeResponse(body: Buffer | string, status = 200, statusText = 'OK'): Awaited<ReturnType<ArchiveFetcher>> {
  return {
    ok: status >= 200 && status < 300,
    status,
    statusText,
    body: Readable.from([Buffer.isBuffer(body) ? body : Buffer.from(body)]),
  };
}

beforeEach(async () => {
  tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'od-installer-archive-'));
  pluginsRoot = path.join(tmpRoot, 'plugins');
  db = new Database(':memory:');
  db.exec(`
    CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT);
    CREATE TABLE conversations (id TEXT PRIMARY KEY, project_id TEXT, title TEXT);
  `);
  migratePlugins(db);
});

afterEach(async () => {
  db.close();
  await rm(tmpRoot, { recursive: true, force: true });
});

describe('archive installer', () => {
  it('extracts a github:owner/repo source via the codeload tarball URL', async () => {
    const tarball = await buildFixtureTarball({ rootPrefix: 'sample-plugin-abc123' });
    let urlSeen = '';
    const fetcher: ArchiveFetcher = async (u) => {
      urlSeen = u;
      return makeFetcher(tarball)('');
    };
    let success = false;
    let error: string | undefined;
    for await (const ev of installPlugin(db, {
      source: 'github:open-design/sample-plugin',
      roots: { userPluginsRoot: pluginsRoot },
      fetcher,
    })) {
      if (ev.kind === 'success') success = true;
      if (ev.kind === 'error') error = ev.message;
    }
    if (!success) {
      throw new Error(`install failed: ${error}`);
    }
    expect(success).toBe(true);
    expect(urlSeen).toBe('https://codeload.github.com/open-design/sample-plugin/tar.gz/HEAD');
    const row = db.prepare(`SELECT source_kind, source FROM installed_plugins WHERE id = 'sample-plugin'`).get();
    expect(row).toEqual({ source_kind: 'github', source: 'github:open-design/sample-plugin' });
  });

  it('extracts a github source with a ref and plugin subpath', async () => {
    const fixtureSrc = path.join(__dirname, 'fixtures', 'plugin-fixtures', 'sample-plugin');
    const fixtureFiles = await readdir(fixtureSrc);
    const urlsSeen: string[] = [];
    const apiUrl =
      'https://api.github.com/repos/nexu-io/open-design/contents/plugins/community/registry-starter?ref=garnet-hemisphere';
    const downloadBase = 'https://raw.example.test/plugins/community/registry-starter';
    const entries = fixtureFiles.map((name) => ({
      type: 'file',
      name,
      path: `plugins/community/registry-starter/${name}`,
      download_url: `${downloadBase}/${name}`,
    }));
    const fileBodies = new Map<string, Buffer>();
    for (const name of fixtureFiles) {
      fileBodies.set(`${downloadBase}/${name}`, await readFile(path.join(fixtureSrc, name)));
    }
    const fetcher: ArchiveFetcher = async (u) => {
      urlsSeen.push(u);
      if (u === apiUrl) return makeResponse(JSON.stringify(entries));
      const body = fileBodies.get(u);
      if (body) return makeResponse(body);
      return makeResponse('not found', 404, 'Not Found');
    };
    let success = false;
    let error: string | undefined;
    const source = 'github:nexu-io/open-design@garnet-hemisphere/plugins/community/registry-starter';
    for await (const ev of installPlugin(db, {
      source,
      roots: { userPluginsRoot: pluginsRoot },
      fetcher,
    })) {
      if (ev.kind === 'success') success = true;
      if (ev.kind === 'error') error = ev.message;
    }
    if (!success) {
      throw new Error(`install failed: ${error}`);
    }
    expect(urlsSeen).toContain(apiUrl);
    expect(urlsSeen).not.toContain('https://codeload.github.com/nexu-io/open-design/tar.gz/garnet-hemisphere');
    const row = db.prepare(`SELECT source_kind, source FROM installed_plugins WHERE id = 'sample-plugin'`).get();
    expect(row).toEqual({ source_kind: 'github', source });
  });

  it.each([
    [403, 'Forbidden', '{"message":"API rate limit exceeded for 127.0.0.1"}'],
    [429, 'Too Many Requests', 'too many requests'],
  ])('falls back to codeload when GitHub contents returns %i for a plugin subpath', async (status, statusText, body) => {
    const tarball = await buildFixtureTarball({
      rootPrefix: 'open-design-main',
      pluginSubpath: 'plugins/community/import-smoke-test',
    });
    const urlsSeen: string[] = [];
    const contentsUrl =
      'https://api.github.com/repos/nexu-io/open-design/contents/plugins/community/import-smoke-test?ref=main';
    const tarballUrl = 'https://codeload.github.com/nexu-io/open-design/tar.gz/main';
    const fetcher: ArchiveFetcher = async (u) => {
      urlsSeen.push(u);
      if (u === contentsUrl) {
        return makeResponse(body, status, statusText);
      }
      if (u === tarballUrl) return makeResponse(tarball);
      return makeResponse('not found', 404, 'Not Found');
    };

    let success = false;
    let error: string | undefined;
    const source = 'github:nexu-io/open-design@main/plugins/community/import-smoke-test';
    for await (const ev of installPlugin(db, {
      source,
      roots: { userPluginsRoot: pluginsRoot },
      fetcher,
    })) {
      if (ev.kind === 'success') success = true;
      if (ev.kind === 'error') error = ev.message;
    }

    if (!success) {
      throw new Error(`install failed: ${error}`);
    }
    expect(urlsSeen).toEqual([contentsUrl, tarballUrl]);
    const row = db.prepare(`SELECT source_kind, source FROM installed_plugins WHERE id = 'sample-plugin'`).get();
    expect(row).toEqual({ source_kind: 'github', source });
  });

  it('reports both GitHub contents and codeload URLs when subpath fallback fails', async () => {
    const urlsSeen: string[] = [];
    const contentsUrl =
      'https://api.github.com/repos/nexu-io/open-design/contents/plugins/community/import-smoke-test?ref=main';
    const tarballUrl = 'https://codeload.github.com/nexu-io/open-design/tar.gz/main';
    const fetcher: ArchiveFetcher = async (u) => {
      urlsSeen.push(u);
      if (u === contentsUrl) {
        return makeResponse('too many requests', 429, 'Too Many Requests');
      }
      if (u === tarballUrl) return makeResponse('server unavailable', 503, 'Service Unavailable');
      return makeResponse('not found', 404, 'Not Found');
    };

    let error: string | undefined;
    const source = 'github:nexu-io/open-design@main/plugins/community/import-smoke-test';
    for await (const ev of installPlugin(db, {
      source,
      roots: { userPluginsRoot: pluginsRoot },
      fetcher,
    })) {
      if (ev.kind === 'error') error = ev.message;
    }

    expect(urlsSeen).toEqual([contentsUrl, tarballUrl]);
    expect(error).toContain('GitHub install failed');
    expect(error).toContain('Fetch failed: 503 Service Unavailable');
    expect(error).toContain(`Tried GitHub fetch URL(s): ${contentsUrl}, ${tarballUrl}`);
  });

  it('extracts a https://*.tgz source (records source_kind=url)', async () => {
    const tarball = await buildFixtureTarball({ rootPrefix: 'sample-plugin-1.0.0' });
    let success = false;
    for await (const ev of installPlugin(db, {
      source: 'https://example.com/sample-plugin-1.0.0.tgz',
      roots: { userPluginsRoot: pluginsRoot },
      fetcher: makeFetcher(tarball),
    })) {
      if (ev.kind === 'success') success = true;
    }
    expect(success).toBe(true);
    const row = db.prepare(`SELECT source_kind, source, archive_integrity FROM installed_plugins WHERE id = 'sample-plugin'`).get() as {
      source_kind: string;
      source: string;
      archive_integrity: string;
    };
    expect(row).toEqual({
      source_kind: 'url',
      source: 'https://example.com/sample-plugin-1.0.0.tgz',
      archive_integrity: `sha256:${createHash('sha256').update(tarball).digest('hex')}`,
    });
  });

  it('rejects archive downloads when marketplace integrity does not match', async () => {
    const tarball = await buildFixtureTarball({ rootPrefix: 'sample-plugin-1.0.0' });
    let success = false;
    let error: string | undefined;
    for await (const ev of installPlugin(db, {
      source: 'https://example.com/sample-plugin-1.0.0.tgz',
      roots: { userPluginsRoot: pluginsRoot },
      fetcher: makeFetcher(tarball),
      archiveIntegrity: 'sha256:deadbeef',
    })) {
      if (ev.kind === 'success') success = true;
      if (ev.kind === 'error') error = ev.message;
    }
    expect(success).toBe(false);
    expect(error).toMatch(/integrity mismatch/);
  });

  it('rejects archives that exceed the size cap', async () => {
    const tarball = await buildFixtureTarball({
      rootPrefix: 'sample-plugin-fat',
      // 2 MiB padding → comfortably above the 64 KiB cap below.
      bigPaddingBytes: 2 * 1024 * 1024,
    });
    let error: string | undefined;
    let success = false;
    for await (const ev of installPlugin(db, {
      source: 'https://example.com/sample.tgz',
      roots: { userPluginsRoot: pluginsRoot },
      fetcher: makeFetcher(tarball),
      maxBytes: 64 * 1024,
    })) {
      if (ev.kind === 'success') success = true;
      if (ev.kind === 'error') error = ev.message;
    }
    expect(success).toBe(false);
    expect(error).toMatch(/exceeds/);
  });

  it('rejects archives containing symlinks', async () => {
    const tarball = await buildFixtureTarball({
      rootPrefix: 'sample-plugin-sym',
      withSymlink: true,
    });
    let success = false;
    let error: string | undefined;
    for await (const ev of installPlugin(db, {
      source: 'https://example.com/sample.tgz',
      roots: { userPluginsRoot: pluginsRoot },
      fetcher: makeFetcher(tarball),
    })) {
      if (ev.kind === 'success') success = true;
      if (ev.kind === 'error') error = ev.message;
    }
    expect(success).toBe(false);
    // tar's strict mode rejects with a clear message; the exact phrasing is
    // less important than the fact that the install never completed.
    expect(error).toBeDefined();
  });

  it('refuses non-tar.gz https sources up-front', async () => {
    let error: string | undefined;
    for await (const ev of installPlugin(db, {
      source: 'https://example.com/sample.zip',
      roots: { userPluginsRoot: pluginsRoot },
      fetcher: makeFetcher(Buffer.alloc(0)),
    })) {
      if (ev.kind === 'error') error = ev.message;
    }
    expect(error).toMatch(/tar\.gz/);
  });
});
