/**
 * Serial (stubs globalThis.fetch): exercises the self-upgrade cache REFRESH
 * orchestration end-to-end — `refreshUpdateCache()` fetches the latest release
 * and writes the correct marker to the shared cache file that the CLI startup
 * hook reads. Network is stubbed; the cache write + marker logic are real.
 *
 * Quarantined as *.serial.test.ts because it reassigns the process-global
 * `fetch` (cross-file-unsafe under the parallel runner).
 */
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { VERSION } from '../src/version.ts';
import { parseSemver } from '../src/core/semver.ts';
import { readUpdateCache } from '../src/core/self-upgrade.ts';
import { refreshUpdateCache } from '../src/commands/check-update.ts';

const realFetch = globalThis.fetch;
let homeDir: string;
let priorHome: string | undefined;

function bump(kind: 'minor' | 'patch' | 'micro'): string {
  const v = parseSemver(VERSION)!;
  if (kind === 'minor') return `${v[0]}.${v[1] + 1}.0.0`;
  if (kind === 'patch') return `${v[0]}.${v[1]}.${v[2] + 1}.0`;
  return `${v[0]}.${v[1]}.${v[2]}.${v[3] + 1}`;
}

function stubReleaseFetch(tag: string | null, ok = true): void {
  globalThis.fetch = (async (url: any) => {
    const u = String(url);
    if (u.includes('/releases/latest')) {
      if (tag === null) throw new Error('network down');
      return new Response(JSON.stringify({ tag_name: tag, published_at: '2026-01-01T00:00:00Z', html_url: 'https://x' }), {
        status: ok ? 200 : 500,
      });
    }
    // Changelog fetch (only happens when update available) — return empty.
    return new Response('', { status: 200 });
  }) as typeof fetch;
}

beforeEach(() => {
  priorHome = process.env.GBRAIN_HOME;
  homeDir = mkdtempSync(join(tmpdir(), 'gbrain-refresh-'));
  process.env.GBRAIN_HOME = homeDir;
});

afterEach(() => {
  globalThis.fetch = realFetch;
  if (priorHome === undefined) delete process.env.GBRAIN_HOME;
  else process.env.GBRAIN_HOME = priorHome;
  rmSync(homeDir, { recursive: true, force: true });
});

describe('refreshUpdateCache — full refresh orchestration (network stubbed)', () => {
  test('minor-bump release → writes upgrade_available marker', async () => {
    const latest = bump('minor');
    stubReleaseFetch(`v${latest}`);
    await refreshUpdateCache();
    const entry = readUpdateCache();
    expect(entry?.marker).toEqual({ kind: 'upgrade_available', current: VERSION, latest });
  });

  test('patch release → writes upgrade_available marker', async () => {
    const latest = bump('patch');
    stubReleaseFetch(`v${latest}`);
    await refreshUpdateCache();
    expect(readUpdateCache()?.marker).toEqual({ kind: 'upgrade_available', current: VERSION, latest });
  });

  test('micro release → writes upgrade_available marker', async () => {
    const latest = bump('micro');
    stubReleaseFetch(`v${latest}`);
    await refreshUpdateCache();
    expect(readUpdateCache()?.marker).toEqual({ kind: 'upgrade_available', current: VERSION, latest });
  });

  test('network failure → writes up_to_date marker (fail-open, TTL prevents hammering)', async () => {
    stubReleaseFetch(null);
    await refreshUpdateCache();
    expect(readUpdateCache()?.marker).toEqual({ kind: 'up_to_date', current: VERSION });
  });

  test('non-OK HTTP → fail-open up_to_date', async () => {
    stubReleaseFetch(`v${bump('minor')}`, false);
    await refreshUpdateCache();
    expect(readUpdateCache()?.marker).toEqual({ kind: 'up_to_date', current: VERSION });
  });

  test('garbage tag → fail-open up_to_date (forged/invalid version never cached as upgrade)', async () => {
    stubReleaseFetch('v$(rm -rf /)');
    await refreshUpdateCache();
    expect(readUpdateCache()?.marker).toEqual({ kind: 'up_to_date', current: VERSION });
  });
});
