/**
 * #2649 review — pipeline-level coverage for the parse-phase heap guardrails.
 *
 * The pure predicates are covered in parse-impl-heap-guard.test.ts; these
 * tests pin the WIRING inside runChunkedParseAndResolve: the mid-loop abort
 * actually rejects the parse with the remedy message (nothing en route may
 * swallow or remap it — the #2441 exit-0 bug class), and the preflight
 * projection is computed from PARSEABLE files, not total scanned files (the
 * miscalibration fixed on this branch).
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';

const getHeapStatisticsMock = vi.hoisted(() => vi.fn());
vi.mock('node:v8', async () => {
  const actual = await vi.importActual<typeof import('node:v8')>('node:v8');
  const mocked = { ...actual, getHeapStatistics: getHeapStatisticsMock };
  return { ...mocked, default: mocked };
});

import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import {
  projectParseHeapNeedBytes,
  runChunkedParseAndResolve,
} from '../../src/core/ingestion/pipeline-phases/parse-impl.js';
import { _captureLogger } from '../../src/core/logger.js';

const MB = 1024 * 1024;

let repoDir: string;
let workerStubPath: string;
let memoryUsageSpy: ReturnType<typeof vi.spyOn> | undefined;

beforeEach(() => {
  delete process.env.GITNEXUS_MEMORY;
  repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-heap-guard-pipeline-'));
  fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
  // The pool validates the worker script's existence up front; the abort test
  // never dispatches, and the preflight test parses one real file.
  workerStubPath = path.join(repoDir, 'fake-worker.js');
  fs.writeFileSync(workerStubPath, '// worker stub for createWorkerPool');
});

afterEach(() => {
  memoryUsageSpy?.mockRestore();
  memoryUsageSpy = undefined;
  delete process.env.GITNEXUS_MEMORY;
  fs.rmSync(repoDir, { recursive: true, force: true });
});

const writeFixture = (rel: string, content: string): { path: string; size: number } => {
  const full = path.join(repoDir, rel);
  fs.writeFileSync(full, content);
  return { path: rel, size: fs.statSync(full).size };
};

describe('#2649 heap guardrails wired into runChunkedParseAndResolve', () => {
  it('mid-loop guard rejects the parse with the actionable remedy message', async () => {
    const file = writeFixture('src/a.ts', 'export function a() { return 1; }\n');
    // 1GB limit with 95% "in use": above the 92% abort threshold.
    getHeapStatisticsMock.mockReturnValue({ heap_size_limit: 1024 * MB });
    memoryUsageSpy = vi.spyOn(process, 'memoryUsage').mockReturnValue({
      rss: 0,
      heapTotal: 1024 * MB,
      heapUsed: 973 * MB,
      external: 0,
      arrayBuffers: 0,
    });

    const graph = createKnowledgeGraph();
    await expect(
      runChunkedParseAndResolve(graph, [file], [file.path], 1, repoDir, Date.now(), () => {}, {
        workerUrlForTest: pathToFileURL(workerStubPath),
        workerPoolSize: 1,
      }),
    ).rejects.toThrow(/Analyze stopped before running out of memory/);
  });

  it('preflight warn projects from PARSEABLE files only and names the parseable count', async () => {
    // Mock the heap limit relative to what ONE parseable file actually projects,
    // so this stays a test of the WARN BEHAVIOUR rather than of the projection
    // constant. Hard-coding 150_000 tied it to PROJECTED_HEAP_BYTES_PER_NODE =
    // 2250; recalibrating that constant for streamed emit (#2680) dropped the
    // projection to 0.80 of the limit and the warn silently stopped firing.
    // Deriving the limit keeps the ratio at 0.90 — above the 0.85 threshold —
    // whatever the constant becomes.
    process.env.GITNEXUS_MEMORY = 'off';
    getHeapStatisticsMock.mockReturnValue({
      heap_size_limit: Math.floor(projectParseHeapNeedBytes(1) / 0.9),
    });

    const parseable = writeFixture('src/b.ts', 'export function b() { return 2; }\n');
    const unparseable = writeFixture('src/data.zzz9', 'not source code\n');

    const cap = _captureLogger();
    const graph = createKnowledgeGraph();
    try {
      await runChunkedParseAndResolve(
        graph,
        [parseable, unparseable],
        [parseable.path, unparseable.path],
        2,
        repoDir,
        Date.now(),
        () => {},
        {
          workerUrlForTest: pathToFileURL(
            path.resolve(
              __dirname,
              '..',
              '..',
              'dist',
              'core',
              'ingestion',
              'workers',
              'parse-worker.js',
            ),
          ),
          workerPoolSize: 1,
        },
      );
    } finally {
      cap.restore();
    }

    const warn = cap.records().find((r) => r.msg.includes('Large repository'));
    // "analyzing 1 files" — the parseable count, not the 2 scanned files.
    expect({
      fired: warn !== undefined,
      parseableBasis: warn?.msg.includes('analyzing 1 files') ?? false,
    }).toEqual({ fired: true, parseableBasis: true });
  });
});
