/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import assert from 'node:assert';
import {existsSync} from 'node:fs';
import {rm} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {describe, it} from 'node:test';

import {
  takeHeapSnapshot,
  getHeapSnapshotSummary,
  getHeapSnapshotDetails,
  getHeapSnapshotClassNodes,
  getHeapSnapshotRetainers,
  closeHeapSnapshot,
  getHeapSnapshotRetainingPaths,
  getHeapSnapshotEdges,
  getHeapSnapshotDominators,
  compareHeapSnapshots,
  getHeapSnapshotDuplicateStrings,
} from '../../src/tools/memory.js';
import {stableIdSymbol} from '../../src/utils/id.js';
import {withMcpContext} from '../utils.js';

describe('memory', () => {
  describe('take_heapsnapshot', () => {
    it('with default options', async () => {
      await withMcpContext(async (response, context) => {
        const filePath = join(tmpdir(), 'test-screenshot.heapsnapshot');
        try {
          await takeHeapSnapshot.handler(
            {params: {filePath}, page: context.getSelectedMcpPage()},
            response,
            context,
          );
          assert.equal(
            response.responseLines.at(0),
            `Heap snapshot saved to ${filePath}`,
          );
          assert.ok(existsSync(filePath));
        } finally {
          await rm(filePath, {force: true});
        }
      });
    });
  });

  describe('get_heapsnapshot_summary', () => {
    it('with default options', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        assert.ok(existsSync(filePath), `Fixture not found at ${filePath}`);

        await getHeapSnapshotSummary.handler(
          {params: {filePath}},
          response,
          context,
        );

        // Call handle to trigger formatting (similar to network tests)
        const responseData = await response.handle(
          getHeapSnapshotSummary.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });
  });

  describe('get_heapsnapshot_details', () => {
    it('with default options', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotDetails.handler(
          {params: {filePath}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotDetails.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('with objectsRetainedByContexts filterName', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotDetails.handler(
          {params: {filePath, filterName: 'objectsRetainedByContexts'}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotDetails.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });
  });

  describe('get_heapsnapshot_class_nodes', () => {
    it('with default options', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await context.getHeapSnapshotAggregates(filePath);

        await getHeapSnapshotClassNodes.handler(
          {params: {filePath, id: 19}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotClassNodes.name,
          context,
        );

        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('with objectsRetainedByContexts filterName', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        const aggregateData = await context.getHeapSnapshotAggregates(
          filePath,
          'objectsRetainedByContexts',
        );
        const aggregate = Object.values(aggregateData.aggregates).find(
          a => a.name === 'Function',
        );
        assert.ok(aggregate);
        const id = aggregate[stableIdSymbol];
        assert.ok(id);

        await getHeapSnapshotClassNodes.handler(
          {params: {filePath, id, filterName: 'objectsRetainedByContexts'}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotClassNodes.name,
          context,
        );

        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('with non-existent class name', async () => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await context.getHeapSnapshotAggregates(filePath);

        await assert.rejects(
          getHeapSnapshotClassNodes.handler(
            {params: {filePath, id: 999999}},
            response,
            context,
          ),
          {message: 'Class with ID 999999 not found in heap snapshot'},
        );
      });
    });
  });

  describe('get_heapsnapshot_retainers', () => {
    it('with valid nodeId', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotRetainers.handler(
          {params: {filePath, nodeId: 25341}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotRetainers.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });
  });

  describe('close_heapsnapshot', () => {
    it('with default options', async () => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotSummary.handler(
          {params: {filePath}},
          response,
          context,
        );

        assert.ok(context.hasHeapSnapshots());

        await closeHeapSnapshot.handler(
          {params: {filePath}},
          response,
          context,
        );

        assert.ok(
          response.responseLines.includes(`Closed heap snapshot: ${filePath}`),
        );
        assert.ok(!context.hasHeapSnapshots());
      });
    });

    it('with non-existent snapshot', async () => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await assert.rejects(
          closeHeapSnapshot.handler({params: {filePath}}, response, context),
          {
            message: `Failed to close heap snapshot: ${filePath} was not loaded.`,
          },
        );
      });
    });
  });

  describe('get_heapsnapshot_retaining_paths', () => {
    it('with valid nodeId', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotRetainingPaths.handler(
          {params: {filePath, nodeId: 45901}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotRetainingPaths.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('reports when limits are reached', async () => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotRetainingPaths.handler(
          {params: {filePath, nodeId: 45901, maxDepth: 1}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotRetainingPaths.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        assert.match(output, /No retaining paths found\./);
        assert.match(
          output,
          /Note: results are truncated, the following limits were reached: depth\./,
        );
      });
    });
  });

  describe('get_heapsnapshot_edges', () => {
    it('with valid nodeId', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotEdges.handler(
          {params: {filePath, nodeId: 25341}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotEdges.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('with pagination', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotEdges.handler(
          {params: {filePath, nodeId: 25341, pageSize: 2}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotEdges.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });
  });

  describe('get_heapsnapshot_dominators', () => {
    it('with valid nodeId', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotDominators.handler(
          {params: {filePath, nodeId: 25341}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotDominators.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });
  });

  describe('compare_heapsnapshots', () => {
    it('compare heap-1 to heap-2', async t => {
      await withMcpContext(async (response, context) => {
        const filePathA = join(
          process.cwd(),
          'tests/fixtures/heap-1.heapsnapshot',
        );
        const filePathB = join(
          process.cwd(),
          'tests/fixtures/heap-2.heapsnapshot',
        );

        await compareHeapSnapshots.handler(
          {params: {baseFilePath: filePathA, currentFilePath: filePathB}},
          response,
          context,
        );

        const responseData = await response.handle(
          compareHeapSnapshots.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('compare heap-2 to heap-3', async t => {
      await withMcpContext(async (response, context) => {
        const filePathA = join(
          process.cwd(),
          'tests/fixtures/heap-2.heapsnapshot',
        );
        const filePathB = join(
          process.cwd(),
          'tests/fixtures/heap-3.heapsnapshot',
        );

        await compareHeapSnapshots.handler(
          {params: {baseFilePath: filePathA, currentFilePath: filePathB}},
          response,
          context,
        );

        const responseData = await response.handle(
          compareHeapSnapshots.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('compare heap-1 to heap-2 with classIndex filter', async t => {
      await withMcpContext(async (response, context) => {
        const filePathA = join(
          process.cwd(),
          'tests/fixtures/heap-1.heapsnapshot',
        );
        const filePathB = join(
          process.cwd(),
          'tests/fixtures/heap-2.heapsnapshot',
        );

        await compareHeapSnapshots.handler(
          {
            params: {
              baseFilePath: filePathA,
              currentFilePath: filePathB,
              classIndex: 2, // NewObject
            },
          },
          response,
          context,
        );

        const responseData = await response.handle(
          compareHeapSnapshots.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });

    it('compare heap-1 to heap-2 with invalid classIndex throws error', async () => {
      await withMcpContext(async (response, context) => {
        const filePathA = join(
          process.cwd(),
          'tests/fixtures/heap-1.heapsnapshot',
        );
        const filePathB = join(
          process.cwd(),
          'tests/fixtures/heap-2.heapsnapshot',
        );

        await assert.rejects(
          compareHeapSnapshots.handler(
            {
              params: {
                baseFilePath: filePathA,
                currentFilePath: filePathB,
                classIndex: 99,
              },
            },
            response,
            context,
          ),
          /Invalid classIndex: 99. Total classes with changes: 10/,
        );
      });
    });
  });

  // Verifies that the caching mechanism in HeapSnapshotManager correctly
  // distinguishes comparisons when the same "current" snapshot is compared
  // against different "base" snapshots. If the cache key (diffCacheKey) is
  // not unique per base snapshot, the second comparison might incorrectly
  // return cached results from the first comparison.
  it('compares the same current snapshot against different bases', async () => {
    await withMcpContext(async (_response, context) => {
      const filePathA = join(
        process.cwd(),
        'tests/fixtures/heap-1.heapsnapshot',
      );
      const filePathB = join(
        process.cwd(),
        'tests/fixtures/heap-2.heapsnapshot',
      );
      const filePathC = join(
        process.cwd(),
        'tests/fixtures/heap-3.heapsnapshot',
      );

      const firstDiff = await context.getHeapSnapshotClassDiffs(
        filePathA,
        filePathC,
      );
      const secondDiff = await context.getHeapSnapshotClassDiffs(
        filePathB,
        filePathC,
      );
      const firstNewObjectDiff = firstDiff.find(
        entry => entry.className === 'NewObject',
      );
      const secondNewObjectDiff = secondDiff.find(
        entry => entry.className === 'NewObject',
      );
      assert.ok(firstNewObjectDiff);
      assert.ok(secondNewObjectDiff);
      assert.equal(firstNewObjectDiff.addedCount, 7);
      assert.equal(secondNewObjectDiff.addedCount, 5);
    });
  });

  describe('get_heapsnapshot_duplicate_strings', () => {
    it('with default options', async t => {
      await withMcpContext(async (response, context) => {
        const filePath = join(
          process.cwd(),
          'tests/fixtures/example.heapsnapshot',
        );

        await getHeapSnapshotDuplicateStrings.handler(
          {params: {filePath}},
          response,
          context,
        );

        const responseData = await response.handle(
          getHeapSnapshotDuplicateStrings.name,
          context,
        );
        const output = responseData.content
          .map(c => (c.type === 'text' ? c.text : ''))
          .join('\n');

        t.assert.snapshot(output);
      });
    });
  });
});
