import * as assert from "assert";
import * as path from "path";
import { rollup, type OutputChunk, type OutputAsset } from "rollup";
import { decode, encode } from "@jridgewell/sourcemap-codec";
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
import dts from "../src/index.js";
import { hydrateSourcemap, type InputSourceMap } from "../src/transform/sourcemap.js";
import { Harness } from "./utils.js";

async function bundleWithSourcemap(inputPath: string) {
  const bundle = await rollup({
    input: inputPath,
    plugins: [dts({ sourcemap: true })],
    onwarn() {},
  });

  const result = await bundle.generate({
    format: "es",
    sourcemap: true,
  });

  return result.output[0];
}

async function bundleMultipleWithSourcemap(
  inputs: Record<string, string>,
  outputDir: string,
): Promise<OutputChunk[]> {
  const bundle = await rollup({
    input: inputs,
    plugins: [dts({ sourcemap: true })],
    onwarn() {},
  });

  const result = await bundle.generate({
    format: "es",
    sourcemap: true,
    dir: outputDir,
  });

  return result.output.filter((o): o is OutputChunk => o.type === "chunk");
}

async function bundleWithAssets(
  inputs: Record<string, string>,
  outputDir: string,
): Promise<{ chunks: OutputChunk[]; assets: OutputAsset[] }> {
  const bundle = await rollup({
    input: inputs,
    plugins: [dts({ sourcemap: true })],
    onwarn() {},
  });

  const result = await bundle.generate({
    format: "es",
    sourcemap: true,
    dir: outputDir,
  });

  return {
    chunks: result.output.filter((o): o is OutputChunk => o.type === "chunk"),
    assets: result.output.filter((o): o is OutputAsset => o.type === "asset"),
  };
}

export default (t: Harness) => {
  t.test("sourcemap/line-mappings-preserved", async () => {
    /**
     * This test verifies that sourcemaps generated by rollup-plugin-dts
     * preserve proper line-by-line mappings for each identifier.
     *
     * The issue: When bundling .d.ts files, the output sourcemap loses
     * detailed mappings. Instead of mapping each identifier to its correct
     * source position, multiple identifiers on the same output line
     * all map to the same position.
     *
     * Input (from tsc):
     *   export type User = { id: string; name: string; };
     *
     * Expected output mappings:
     *   - 'type' maps to 'export' or 'type' in source
     *   - 'User' maps to 'User' in source
     *   - 'id' maps to 'id' in source
     *   - 'name' maps to 'name' in source
     *
     * Actual (broken) output mappings:
     *   - All identifiers on line 1 map to same position (col 7)
     *   - Lines 2+ have minimal or no mappings
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "index.d.ts"));
    const map = chunk.map!;

    // Parse the mappings to check individual segment positions
    // decode returns: [generatedCol, sourceIndex, sourceLine, sourceCol, nameIndex?][][]
    const decoded = decode(map.mappings);

    // The output code should be:
    // Line 1: type User = {
    // Line 2:     id: string;
    // Line 3:     name: string;
    // Line 4: };
    // Line 5: (empty)
    // Line 6: export type { User };

    // Check that line 1 has multiple distinct column mappings
    // (not all pointing to the same source position)
    const line1 = decoded[0] || [];
    assert.ok(
      line1.length >= 2,
      `Line 1 should have at least 2 mapping segments, got ${line1.length}. ` +
        `Mappings: ${map.mappings}`,
    );

    // The segments on line 1 should map to different source columns
    // If they all map to the same column, the sourcemap is broken
    const line1SourceCols = line1.map((seg) => seg[3]); // sourceColumn is index 3
    const uniqueSourceCols = new Set(line1SourceCols);
    assert.ok(
      uniqueSourceCols.size > 1,
      `Line 1 segments should map to different source columns, but all map to: ${[...uniqueSourceCols].join(", ")}. ` +
        `This breaks Go-to-Definition in VSCode.`,
    );

    // Check that line 2 (id: string;) has mapping segments
    const line2 = decoded[1] || [];
    assert.ok(
      line2.length >= 1,
      `Line 2 should have mapping segments for 'id: string;', got ${line2.length}`,
    );

    // Check that line 3 (name: string;) has mapping segments
    const line3 = decoded[2] || [];
    assert.ok(
      line3.length >= 1,
      `Line 3 should have mapping segments for 'name: string;', got ${line3.length}`,
    );

    // Most importantly: verify the mappings point to correct source lines
    // Line 2 in output (id: string;) should map to line 2 in source
    if (line2.length > 0 && line2[0]!.length >= 4) {
      const sourceLine = line2[0]![2]!; // sourceLine is index 2
      assert.strictEqual(
        sourceLine,
        1, // 0-indexed, so line 2 is index 1
        `Line 2 'id: string;' should map to source line 2, got ${sourceLine + 1}`,
      );
    }

    // Line 3 in output (name: string;) should map to line 3 in source
    if (line3.length > 0 && line3[0]!.length >= 4) {
      const sourceLine = line3[0]![2]!; // sourceLine is index 2
      assert.strictEqual(
        sourceLine,
        2, // 0-indexed, so line 3 is index 2
        `Line 3 'name: string;' should map to source line 3, got ${sourceLine + 1}`,
      );
    }
  });

  t.test("sourcemap/input-map-chained", async () => {
    /**
     * This test verifies that when input .d.ts files have associated
     * .d.ts.map files, the output sourcemap chains through to the
     * original source file.
     *
     * The input .d.ts.map file maps:
     *   index.d.ts -> ../src/index.ts
     *
     * The output sourcemap should ultimately map:
     *   output.d.ts -> ../src/index.ts (the original source)
     *
     * NOT:
     *   output.d.ts -> index.d.ts (the intermediate file)
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "index.d.ts"));
    const map = chunk.map!;

    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("index.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `Sourcemap should chain to original source (*.ts), but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/source-root-resolved", async () => {
    /**
     * This test verifies that sourceRoot in input sourcemaps is respected.
     *
     * The input .d.ts.map has:
     *   sourceRoot: "../src-root"
     *   sources: ["types.ts"]
     *
     * The output sourcemap should point to ../src-root/types.ts, not
     * resolve sources relative to the .d.ts file itself.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "source-root.d.ts"));
    const map = chunk.map!;

    const hasSourceRoot = map.sources.some((source) => source.replace(/\\\\/g, "/").endsWith("src-root/types.ts"));
    assert.ok(
      hasSourceRoot,
      `Sourcemap should resolve sourceRoot to src-root/types.ts, but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/inline-base64", async () => {
    /**
     * This test verifies that inline base64-encoded sourcemaps are correctly
     * parsed and chained to the original source file.
     *
     * The input file has a sourceMappingURL comment with a data URL:
     *   //# sourceMappingURL=data:application/json;base64,...
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "inline-base64.d.ts"));
    const map = chunk.map!;

    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("index.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `Inline base64 sourcemap should chain to original source, but sources: ${map.sources.join(", ")}`,
    );

    // Verify line mappings are preserved
    const decoded = decode(map.mappings);
    const linesWithMappings = decoded.filter((line) => line.length > 0).length;
    assert.ok(
      linesWithMappings >= 4,
      `Should have at least 4 lines with mappings, got ${linesWithMappings}`,
    );
  });

  t.test("sourcemap/inline-urlencoded", async () => {
    /**
     * This test verifies that inline URL-encoded sourcemaps are correctly
     * parsed and chained to the original source file.
     *
     * The input file has a sourceMappingURL comment with a data URL:
     *   //# sourceMappingURL=data:application/json,%7B%22version%22...
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "inline-urlencoded.d.ts"));
    const map = chunk.map!;

    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("index.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `Inline URL-encoded sourcemap should chain to original source, but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/file-reference", async () => {
    /**
     * This test verifies that sourceMappingURL file references (not adjacent
     * .d.ts.map files) are correctly resolved and chained.
     *
     * The input file has:
     *   //# sourceMappingURL=maps/types.d.ts.map
     *
     * The map file is in a subdirectory, not adjacent to the .d.ts file.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/file-ref");
    const chunk = await bundleWithSourcemap(path.join(dir, "types.d.ts"));
    const map = chunk.map!;

    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("index.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `File reference sourcemap should chain to original source, but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/query-string-stripped", async () => {
    /**
     * This test verifies that query strings in sourceMappingURL comments
     * are stripped when resolving the map file path.
     *
     * The input file has:
     *   //# sourceMappingURL=query-string.d.ts.map?v=12345
     *
     * The actual file is query-string.d.ts.map (without query string).
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "query-string.d.ts"));
    const map = chunk.map!;

    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("index.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `Query string should be stripped from sourceMappingURL, but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/hash-fragment-stripped", async () => {
    /**
     * This test verifies that hash fragments in sourceMappingURL comments
     * are stripped when resolving the map file path.
     *
     * The input file has:
     *   //# sourceMappingURL=hash-fragment.d.ts.map#v12345
     *
     * The actual file is hash-fragment.d.ts.map (without hash fragment).
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "hash-fragment.d.ts"));
    const map = chunk.map!;

    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("index.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `Hash fragment should be stripped from sourceMappingURL, but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/go-to-definition-identifiers", async () => {
    /**
     * This test verifies that Go-to-Definition works for individual identifiers
     * within type definitions, not just line starts.
     *
     * Given output like:
     *   type User = {
     *       id: string;
     *       name: string;
     *   };
     *
     * Clicking on "id" (line 2, col 4) should map to "id" in the original source,
     * NOT to the start of the line (col 0).
     *
     * This is critical for IDE navigation - without per-identifier mappings,
     * Go-to-Definition jumps to wrong positions.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "index.d.ts"));
    const map = chunk.map!;
    const code = chunk.code;

    // Find positions of identifiers in the generated code
    const lines = code.split("\n");

    // Line 2 should be "    id: string;" - find column of "id"
    const line2 = lines[1]!;
    const idCol = line2.indexOf("id");
    assert.ok(idCol >= 0, `Could not find "id" in line 2: "${line2}"`);

    // Line 3 should be "    name: string;" - find column of "name"
    const line3 = lines[2]!;
    const nameCol = line3.indexOf("name");
    assert.ok(nameCol >= 0, `Could not find "name" in line 3: "${line3}"`);

    // Use trace-mapping to simulate Go-to-Definition
    // This is exactly what IDEs do when you click on an identifier
    const tracer = new TraceMap(map as any);

    // Query position for "id" - line is 1-based in trace-mapping API, column is 0-based
    const idPosition = originalPositionFor(tracer, { line: 2, column: idCol });

    // The mapping should point to a column > 0 in the source
    // If it points to col 0, Go-to-Definition will jump to line start, not "id"
    assert.ok(
      idPosition.column !== null && idPosition.column > 0,
      `"id" at output line 2, col ${idCol} should map to source column > 0, ` +
        `but maps to column ${idPosition.column}. ` +
        `This breaks Go-to-Definition - it will jump to line start instead of the identifier.`,
    );

    // Query position for "name"
    const namePosition = originalPositionFor(tracer, { line: 3, column: nameCol });

    assert.ok(
      namePosition.column !== null && namePosition.column > 0,
      `"name" at output line 3, col ${nameCol} should map to source column > 0, ` +
        `but maps to column ${namePosition.column}. ` +
        `This breaks Go-to-Definition.`,
    );
  });

  t.test("sourcemap/subdirectory-paths", async () => {
    /**
     * This test verifies that sourcemap paths are correctly relative to the
     * chunk's output location, not the output directory root.
     *
     * When output is:
     *   dist/subdir/types.d.ts
     *   dist/subdir/types.d.ts.map
     *
     * And the original source is at:
     *   src/types.ts
     *
     * The sourcemap should have:
     *   sources: ["../../src/types.ts"]  (relative to dist/subdir/)
     *
     * NOT:
     *   sources: ["../src/types.ts"]  (relative to dist/)
     *
     * This is critical for Go-to-Definition when types are in subdirectories.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunks = await bundleMultipleWithSourcemap(
      { "subdir/types.d.ts": path.join(dir, "subdir/types.d.ts") },
      dir,
    );

    // Rollup adds .d.ts extension, so filename becomes subdir/types.d.ts.d.ts
    const chunk = chunks.find((c) => c.fileName.includes("subdir/"));
    assert.ok(chunk, `Could not find subdir chunk, got: ${chunks.map((c) => c.fileName).join(", ")}`);

    const map = chunk.map!;

    // The source path should be relative to the chunk's directory (subdir/),
    // not the output root. From subdir/, we need ../../src/types.ts to reach src/
    const hasCorrectPath = map.sources.some((source) => source.startsWith("../../src/"));
    assert.ok(
      hasCorrectPath,
      `Sourcemap paths should be relative to chunk directory (../../src/...), ` +
        `but got sources: ${map.sources.join(", ")}. ` +
        `This breaks Go-to-Definition for types in subdirectories.`,
    );

    // Also verify it's not the incorrect path (relative to output root)
    const hasIncorrectPath = map.sources.some(
      (source) => source.startsWith("../src/") && !source.startsWith("../../"),
    );
    assert.ok(
      !hasIncorrectPath,
      `Sourcemap paths should NOT be relative to output root (../src/...), ` +
        `but got sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/file-field-is-basename", async () => {
    /**
     * This test verifies that the "file" field in sourcemaps is just the
     * basename of the output file, not a path with directories.
     *
     * When output is:
     *   dist/subdir/types.d.ts
     *   dist/subdir/types.d.ts.map
     *
     * The sourcemap's "file" field should be:
     *   "file": "types.d.ts"  (just basename)
     *
     * NOT:
     *   "file": "subdir/types.d.ts"  (includes directory)
     *
     * The "file" field is relative to the .map file's location. Since the .map
     * file is in the same directory as the .d.ts file, "file" should just be
     * the basename. Including the directory path breaks VSCode Go-to-Definition.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const { chunks, assets } = await bundleWithAssets(
      { "subdir/types.d.ts": path.join(dir, "subdir/types.d.ts") },
      dir,
    );

    const chunk = chunks.find((c) => c.fileName.includes("subdir/"));
    assert.ok(chunk, `Could not find subdir chunk`);

    // Find the .map asset for this chunk
    const mapAsset = assets.find((a) => a.fileName === `${chunk.fileName}.map`);
    assert.ok(mapAsset, `Could not find map asset for ${chunk.fileName}`);

    // Parse the actual asset source to get the file field
    const mapContent = JSON.parse(mapAsset.source as string);

    // The file field should be just the basename, not include directories
    // chunk.fileName might be "subdir/types.d.ts.d.ts" but map.file should be "types.d.ts.d.ts"
    const expectedBasename = path.basename(chunk.fileName);
    assert.strictEqual(
      mapContent.file,
      expectedBasename,
      `Sourcemap "file" field should be basename "${expectedBasename}", ` +
        `but got "${mapContent.file}". ` +
        `This breaks VSCode Go-to-Definition for types in subdirectories.`,
    );

    // Verify it doesn't contain path separators
    assert.ok(
      !mapContent.file?.includes("/") && !mapContent.file?.includes("\\"),
      `Sourcemap "file" field should not contain path separators, but got "${mapContent.file}"`,
    );
  });

  t.test("sourcemap/empty-export-preserves-sources", async () => {
    /**
     * This test verifies that empty chunks (like "export {};") preserve
     * source references from the input sourcemap.
     *
     * When a .d.ts file contains only "export {};", Rollup produces an
     * empty sourcemap with sources: []. However, the input .d.ts.map
     * may have valid source references that should be preserved.
     *
     * This is important for Go-to-Definition on re-export barrel files.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "empty-export.d.ts"));
    const map = chunk.map!;

    // The output code should be "export {};" or similar
    assert.ok(
      chunk.code.includes("export") || chunk.code.trim() === "",
      `Expected empty export output, got: ${chunk.code}`,
    );

    // Even though the output is empty, sources should be preserved from input map
    assert.ok(
      map.sources.length > 0,
      `Empty export should preserve sources from input map, but got sources: []`,
    );

    // The source should point to the original .ts file
    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("index.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `Empty export should chain to original source, but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/hydrate-uses-first-mapped-segment", async () => {
    /**
     * This test verifies that hydrateSourcemap uses the first mapped segment
     * on a line as the anchor, not just the first segment.
     *
     * If the first segment is unmapped (length 1), but a later segment maps
     * to a source, hydration should still occur.
     */

    const sparseMappings = encode([
      [
        [0], // unmapped segment at column 0
        [2, 0, 0, 0], // first mapped segment at column 2
      ],
    ]);

    const inputMap: InputSourceMap = {
      version: 3,
      sources: ["source.ts"],
      sourcesContent: ["type User = { id: string; }"],
      mappings: encode([
        [
          [0, 0, 0, 0],
          [4, 0, 0, 4],
        ],
      ]),
      names: [],
    };

    const hydrated = hydrateSourcemap(sparseMappings, inputMap, "xxUser");
    const decoded = decode(hydrated);
    const line = decoded[0] || [];

    const mappedSegments = line.filter((seg) => seg.length >= 4);
    assert.strictEqual(
      mappedSegments.length,
      2,
      `Expected 2 mapped segments after hydration, got ${mappedSegments.length}. Mappings: ${hydrated}`,
    );
  });

  t.test("sourcemap/null-source-in-input-map", async () => {
    /**
     * This test verifies that input sourcemaps with null sources do not crash
     * during path normalization and are preserved in the output map.
     *
     * Note: We intentionally don't emit sourcesContent (TypeScript never emits it
     * for declaration maps, and tsserver rejects maps with string entries).
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "null-source.d.ts"));
    const map = chunk.map!;

    assert.ok(
      (map.sources as Array<string | null>).some((source) => source === null),
      `Expected output sourcemap to preserve null source, but sources: ${map.sources.join(", ")}`,
    );

    // sourcesContent should not be present in output (stripped for TS compatibility)
    assert.strictEqual(
      (map as any).sourcesContent,
      undefined,
      "sourcesContent should not be present in output map",
    );
  });

  t.test("sourcemap/url-sourceroot-preserved", async () => {
    /**
     * This test verifies that URL sourceRoot values are preserved correctly.
     *
     * When the input .d.ts.map has:
     *   sourceRoot: "https://example.com/src/"
     *   sources: ["index.ts"]
     *
     * The output sourcemap should have:
     *   sources: ["https://example.com/src/index.ts"]
     *
     * NOT corrupted paths like:
     *   sources: ["/path/to/https:/example.com/src/index.ts"]
     *
     * URL sourceRoot values are used when pointing to external source repositories,
     * such as GitHub source links. They must be preserved verbatim.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "url-sourceroot.d.ts"));
    const map = chunk.map!;

    // The output source should be the URL sourceRoot + relative source
    const hasUrlSource = map.sources.some((source) => source === "https://example.com/src/index.ts");
    assert.ok(
      hasUrlSource,
      `URL sourceRoot should be preserved, expected "https://example.com/src/index.ts", ` +
        `but sources: ${map.sources.join(", ")}`,
    );

    // Verify it's not corrupted by path.resolve()
    const hasCorruptedPath = map.sources.some(
      (source) => source.includes("/https:") || source.includes("\\https:"),
    );
    assert.ok(
      !hasCorruptedPath,
      `URL sourceRoot should not be corrupted by path.resolve(), ` + `but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/url-sourceroot-normalizes-dotdot", async () => {
    /**
     * This test verifies that URL sourceRoot with ../ in sources is normalized.
     *
     * When the input .d.ts.map has:
     *   sourceRoot: "https://example.com/dist/types/"
     *   sources: ["../../src/index.ts"]
     *
     * The output sourcemap should have normalized URL:
     *   sources: ["https://example.com/src/index.ts"]
     *
     * NOT the un-normalized path:
     *   sources: ["https://example.com/dist/types/../../src/index.ts"]
     *
     * This is important for cleaner sourcemaps and correct resolution.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "url-sourceroot-dotdot.d.ts"));
    const map = chunk.map!;

    // The output source should be the normalized URL (../ resolved)
    const hasNormalizedUrl = map.sources.some((source) => source === "https://example.com/src/index.ts");
    assert.ok(
      hasNormalizedUrl,
      `URL sourceRoot with ../ should be normalized to "https://example.com/src/index.ts", ` +
        `but sources: ${map.sources.join(", ")}`,
    );

    // Verify ../ is not present in the output (it should be resolved)
    const hasUnresolvedDotDot = map.sources.some((source) => source.includes("/../") || source.includes("/.."));
    assert.ok(
      !hasUnresolvedDotDot,
      `URL path should not contain unresolved ../, but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/url-source-preserved", async () => {
    /**
     * This test verifies that URL sources (like file:// URLs) are preserved.
     *
     * When the input .d.ts.map has:
     *   sources: ["file:///absolute/path/to/index.ts"]
     *
     * The output sourcemap should preserve the URL as-is:
     *   sources: ["file:///absolute/path/to/index.ts"]
     *
     * NOT converted to a relative path like:
     *   sources: ["../../../absolute/path/to/index.ts"]
     *
     * TypeScript can emit file:// URLs for sources when using mapRoot
     * configurations. These must be preserved.
     *
     * Note: This test uses a POSIX-style file:// URL in the fixture. Since we're
     * testing URL passthrough (not generation), this works cross-platform - we
     * just verify the URL passes through unchanged regardless of the host OS.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures");
    const chunk = await bundleWithSourcemap(path.join(dir, "url-source.d.ts"));
    const map = chunk.map!;

    // The output source should preserve the file:// URL
    const hasFileUrl = map.sources.some((source) => source === "file:///absolute/path/to/index.ts");
    assert.ok(
      hasFileUrl,
      `file:// URL source should be preserved, expected "file:///absolute/path/to/index.ts", ` +
        `but sources: ${map.sources.join(", ")}`,
    );

    // Verify it's not converted to a relative path
    const hasRelativePath = map.sources.some(
      (source) => source.startsWith("../") || source.startsWith("./"),
    );
    assert.ok(
      !hasRelativePath,
      `URL source should not be converted to relative path, ` + `but sources: ${map.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/multi-source-composition", async () => {
    /**
     * This test verifies that bundling multiple input files produces a
     * sourcemap with correct multi-source handling.
     *
     * When bundling:
     *   - index.d.ts (re-exports from user.d.ts and post.d.ts)
     *   - user.d.ts -> ../src/user.ts
     *   - post.d.ts -> ../src/post.ts
     *
     * The output sourcemap should have:
     *   sources: ["../src/user.ts", "../src/post.ts"] (or similar)
     *
     * With mapping segments using correct source indices:
     *   - User type mappings should point to user.ts (source index 0 or 1)
     *   - Post type mappings should point to post.ts (different source index)
     *
     * This tests the remapping path (not hydration) since multiple sources
     * cannot use the simple single-source optimization.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/multi-source");
    const chunk = await bundleWithSourcemap(path.join(dir, "index.d.ts"));
    const map = chunk.map!;
    const code = chunk.code;

    // Should have multiple sources pointing to original .ts files
    const userSource = map.sources.find((s) => s.includes("user.ts"));
    const postSource = map.sources.find((s) => s.includes("post.ts"));

    assert.ok(
      userSource,
      `Output map should include user.ts in sources, but sources: ${map.sources.join(", ")}`,
    );
    assert.ok(
      postSource,
      `Output map should include post.ts in sources, but sources: ${map.sources.join(", ")}`,
    );

    // Verify sources are distinct (not merged into one)
    assert.ok(
      userSource !== postSource,
      `user.ts and post.ts should be distinct sources, ` +
        `but got: user=${userSource}, post=${postSource}`,
    );

    // Decode mappings to verify source indices are used correctly
    const decoded = decode(map.mappings);

    // Find source indices
    const userSourceIndex = map.sources.indexOf(userSource!);
    const postSourceIndex = map.sources.indexOf(postSource!);

    // Find lines containing User and Post in the output
    const lines = code.split("\n");
    const userLineIndex = lines.findIndex((l) => l.includes("User"));
    const postLineIndex = lines.findIndex((l) => l.includes("Post"));

    assert.ok(userLineIndex >= 0, `Could not find "User" in output code`);
    assert.ok(postLineIndex >= 0, `Could not find "Post" in output code`);

    // Check that User line maps to user.ts source
    const userLineMappings = decoded[userLineIndex] || [];
    const userMappedSegment = userLineMappings.find((seg) => seg.length >= 4);
    assert.ok(
      userMappedSegment,
      `User line (${userLineIndex}) should have a mapped segment, but found none. ` +
        `Mappings: ${map.mappings}`,
    );
    assert.strictEqual(
      userMappedSegment[1],
      userSourceIndex,
      `User type should map to user.ts (index ${userSourceIndex}), ` +
        `but maps to source index ${userMappedSegment[1]}`,
    );

    // Check that Post line maps to post.ts source
    const postLineMappings = decoded[postLineIndex] || [];
    const postMappedSegment = postLineMappings.find((seg) => seg.length >= 4);
    assert.ok(
      postMappedSegment,
      `Post line (${postLineIndex}) should have a mapped segment, but found none. ` +
        `Mappings: ${map.mappings}`,
    );
    assert.strictEqual(
      postMappedSegment[1],
      postSourceIndex,
      `Post type should map to post.ts (index ${postSourceIndex}), ` +
        `but maps to source index ${postMappedSegment[1]}`,
    );
  });

  t.test("sourcemap/ts-input-direct-compilation", async () => {
    /**
     * This test verifies that when bundling .ts files directly (not .d.ts),
     * sourcemaps work correctly when dts({ sourcemap: true }) is used.
     *
     * Scenario: User has .ts files and wants rollup-plugin-dts to generate
     * .d.ts bundles with sourcemaps pointing back to the original .ts files.
     *
     * Without the sourcemap option:
     *   - TypeScript's declarationMap is disabled during emit
     *   - No .d.ts.map is generated in-memory
     *   - Output sourcemap has no input map to chain
     *   - Go-to-Definition points to intermediate .d.ts (if it exists) or fails
     *
     * With dts({ sourcemap: true }):
     *   - TypeScript's declarationMap is enabled during emit
     *   - .d.ts.map is captured in-memory and passed to transform
     *   - Output sourcemap chains through to original .ts source
     *   - Go-to-Definition works correctly
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/src");
    const chunk = await bundleWithSourcemap(path.join(dir, "user.ts"));
    const map = chunk.map!;
    const code = chunk.code;

    // The output sourcemap should point to the original .ts file, not .d.ts
    const hasOriginalTsSource = map.sources.some(
      (source) => source.endsWith("user.ts") && !source.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalTsSource,
      `Sourcemap should point to original .ts file, but sources: ${map.sources.join(", ")}. ` +
        `When using dts({ sourcemap: true }), Go-to-Definition should navigate to .ts sources.`,
    );

    // Verify per-identifier mappings work (not just line-level)
    const decoded = decode(map.mappings);

    // The output should have type User = { ... }
    // Find the line with "User" and verify it has multiple mapped segments
    const lines = code.split("\n");
    const userLineIndex = lines.findIndex((l) => l.includes("User"));
    assert.ok(userLineIndex >= 0, `Could not find "User" in output code: ${code}`);

    const userLineMappings = decoded[userLineIndex] || [];
    assert.ok(
      userLineMappings.length >= 1,
      `User line should have mapping segments, got ${userLineMappings.length}. ` +
        `This indicates the declaration map was not properly captured during TypeScript emit.`,
    );

    // Verify "id" and "name" lines also have mappings
    const idLineIndex = lines.findIndex((l) => l.includes("id:"));
    const nameLineIndex = lines.findIndex((l) => l.includes("name:"));

    if (idLineIndex >= 0) {
      const idLineMappings = decoded[idLineIndex] || [];
      assert.ok(
        idLineMappings.length >= 1,
        `"id:" line should have mapping segments for Go-to-Definition to work`,
      );
    }

    if (nameLineIndex >= 0) {
      const nameLineMappings = decoded[nameLineIndex] || [];
      assert.ok(
        nameLineMappings.length >= 1,
        `"name:" line should have mapping segments for Go-to-Definition to work`,
      );
    }

    // Use trace-mapping to verify Go-to-Definition would work
    const tracer = new TraceMap(map as any);

    // Find column of "User" in the output
    const userLine = lines[userLineIndex]!;
    const userCol = userLine.indexOf("User");
    assert.ok(userCol >= 0, `Could not find "User" column in line: "${userLine}"`);

    // Query position for "User" - should map to the original .ts source
    const userPosition = originalPositionFor(tracer, { line: userLineIndex + 1, column: userCol });
    assert.ok(
      userPosition.source !== null,
      `"User" should map to a source, but got null. ` +
        `This breaks Go-to-Definition for .ts input files.`,
    );
    assert.ok(
      userPosition.source!.endsWith("user.ts"),
      `"User" should map to user.ts, but got: ${userPosition.source}`,
    );
  });

  t.test("sourcemap/mts-input-direct-compilation", async () => {
    /**
     * This test verifies that .mts inputs work with dts({ sourcemap: true }).
     * The fallback lookup must convert .mts → .d.ts (matching getDeclarationId behavior).
     *
     * Note: Per-identifier precision varies by TypeScript version. This test
     * verifies source chaining works; ts-input-direct-compilation tests precision.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/src");
    const chunk = await bundleWithSourcemap(path.join(dir, "module.mts"));
    const map = chunk.map!;

    // The output sourcemap should point to the original .mts file
    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("module.mts"),
    );
    assert.ok(
      hasOriginalSource,
      `Sourcemap should point to original .mts file, but sources: ${map.sources.join(", ")}`,
    );

    // Verify mappings exist (precision varies by TS version)
    const decoded = decode(map.mappings);
    const linesWithMappings = decoded.filter((line) => line.length > 0).length;
    assert.ok(
      linesWithMappings >= 1,
      `Should have at least 1 line with mappings, got ${linesWithMappings}`,
    );
  });

  t.test("sourcemap/tsx-input-direct-compilation", async () => {
    /**
     * This test verifies that .tsx inputs work with dts({ sourcemap: true }).
     * The fallback lookup must convert .tsx → .d.ts (matching getDeclarationId behavior).
     *
     * Note: Per-identifier precision varies by TypeScript version. This test
     * verifies source chaining works; ts-input-direct-compilation tests precision.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/src");
    const chunk = await bundleWithSourcemap(path.join(dir, "component.tsx"));
    const map = chunk.map!;

    // The output sourcemap should point to the original .tsx file
    const hasOriginalSource = map.sources.some(
      (source) => source.endsWith("component.tsx"),
    );
    assert.ok(
      hasOriginalSource,
      `Sourcemap should point to original .tsx file, but sources: ${map.sources.join(", ")}`,
    );

    // Verify mappings exist (precision varies by TS version)
    const decoded = decode(map.mappings);
    const linesWithMappings = decoded.filter((line) => line.length > 0).length;
    assert.ok(
      linesWithMappings >= 1,
      `Should have at least 1 line with mappings, got ${linesWithMappings}`,
    );
  });

  t.test("sourcemap/opt-out-with-rollup-sourcemap-enabled", async () => {
    /**
     * When dts({ sourcemap: false }), the plugin returns map: null from
     * transform (no detailed sourcemaps). Rollup may still produce basic
     * line-level mappings in the output since output.sourcemap is true,
     * but the plugin should not warn about missing sourcemaps.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/src");
    const inputPath = path.join(dir, "user.ts");

    const warnings: string[] = [];
    const bundle = await rollup({
      input: inputPath,
      plugins: [dts({ sourcemap: false })],
      onwarn(warning) {
        warnings.push(String(warning.message || warning));
      },
    });

    const result = await bundle.generate({
      format: "es",
      sourcemap: true,
    });

    const chunk = result.output[0];

    // The plugin should not trigger "sourcemap is likely to be incorrect" warnings
    const sourcemapWarnings = warnings.filter((w) => w.includes("sourcemap"));
    assert.strictEqual(
      sourcemapWarnings.length,
      0,
      `Expected no sourcemap warnings, got: ${sourcemapWarnings.join("; ")}`,
    );

    // Output should still have a map object (from Rollup's output.sourcemap: true)
    assert.ok(chunk.map, "Expected a map object in output when output.sourcemap is true");
  });

  t.test("sourcemap/compiler-override-precedence", async () => {
    /**
     * This test verifies that dts({ sourcemap: true }) takes precedence
     * over compilerOptions.declarationMap: false.
     *
     * Capability-driven approach: We compare against a baseline (sourcemap: true
     * without override) to verify the override produces identical results.
     * This works across all TypeScript versions without version checks.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/src");
    const inputPath = path.join(dir, "user.ts");

    // Baseline: Bundle with just sourcemap: true
    const baselineBundle = await rollup({
      input: inputPath,
      plugins: [dts({ sourcemap: true })],
      onwarn() {},
    });
    const baselineResult = await baselineBundle.generate({ format: "es", sourcemap: true });
    const baselineMap = baselineResult.output[0].map!;
    const baselineSegments = decode(baselineMap.mappings).reduce((sum, line) => sum + line.length, 0);

    // Override case: Bundle with sourcemap: true + declarationMap: false
    const overrideBundle = await rollup({
      input: inputPath,
      plugins: [
        dts({
          sourcemap: true,
          compilerOptions: {
            declarationMap: false, // This should be overridden by sourcemap: true
          },
        }),
      ],
      onwarn() {},
    });
    const overrideResult = await overrideBundle.generate({ format: "es", sourcemap: true });
    const overrideMap = overrideResult.output[0].map!;
    const overrideSegments = decode(overrideMap.mappings).reduce((sum, line) => sum + line.length, 0);

    // The override should produce the same precision as the baseline
    // (whatever TypeScript's declarationMap naturally produces)
    assert.strictEqual(
      overrideSegments,
      baselineSegments,
      `sourcemap: true should override declarationMap: false. ` +
        `Baseline has ${baselineSegments} segments, override has ${overrideSegments}. ` +
        `They should be equal.`,
    );

    // Verify source chaining works
    const hasOriginalSource = overrideMap.sources.some(
      (s) => s.endsWith("user.ts") && !s.endsWith(".d.ts"),
    );
    assert.ok(
      hasOriginalSource,
      `sourcemap: true should chain to original .ts, but got: ${overrideMap.sources.join(", ")}`,
    );
  });

  t.test("sourcemap/mixed-inputs-dts-and-ts", async () => {
    /**
     * This test verifies that bundling both .d.ts (with external map) and
     * .ts (with in-memory mapContent) in the same bundle works correctly.
     *
     * Fixture structure:
     *   - index.ts imports from ./types.js and ./utils.js
     *   - types.d.ts has external .d.ts.map → ../src/types.ts
     *   - utils.ts will be compiled via generateDts() with in-memory mapContent
     *
     * Both paths should produce proper sourcemap chains with no cross-contamination.
     */

    const dir = path.resolve(process.cwd(), "tests/sourcemap-fixtures/mixed-inputs");

    // Bundle index.ts which imports from both .d.ts (external map) and .ts (compiled)
    const bundle = await rollup({
      input: path.join(dir, "index.ts"),
      plugins: [dts({ sourcemap: true })],
      onwarn() {},
    });

    const result = await bundle.generate({
      format: "es",
      sourcemap: true,
    });

    const chunk = result.output[0];
    const map = chunk.map!;

    // Should have sources from both:
    // - types.ts (from external .d.ts.map chain)
    // - utils.ts (from in-memory mapContent via generateDts)
    const typesSource = map.sources.find((s) => s.includes("types.ts"));
    const utilsSource = map.sources.find((s) => s.includes("utils.ts"));

    assert.ok(
      typesSource,
      `Mixed bundle should include types.ts (from external map chain) in sources: ${map.sources.join(", ")}`,
    );
    assert.ok(
      utilsSource,
      `Mixed bundle should include utils.ts (from in-memory mapContent) in sources: ${map.sources.join(", ")}`,
    );

    // Sources should be distinct (no cross-contamination)
    assert.ok(
      typesSource !== utilsSource,
      `Sources should be distinct, got types=${typesSource}, utils=${utilsSource}`,
    );

    // Verify mappings point to correct sources
    const decoded = decode(map.mappings);
    const lines = chunk.code.split("\n");

    // Find line with "Config" (from types.d.ts → types.ts)
    const configLineIndex = lines.findIndex((l) => l.includes("Config"));
    // Find line with "Helper" (from utils.ts)
    const helperLineIndex = lines.findIndex((l) => l.includes("Helper"));

    // Both Config and Helper lines must exist in output
    assert.ok(configLineIndex >= 0, `Could not find "Config" in output code`);
    assert.ok(helperLineIndex >= 0, `Could not find "Helper" in output code`);

    const typesSourceIndex = map.sources.indexOf(typesSource!);
    const utilsSourceIndex = map.sources.indexOf(utilsSource!);

    const configMappings = decoded[configLineIndex] || [];
    const helperMappings = decoded[helperLineIndex] || [];

    const configSegment = configMappings.find((seg) => seg.length >= 4);
    const helperSegment = helperMappings.find((seg) => seg.length >= 4);

    // Both segments must exist for proper sourcemap functionality
    assert.ok(
      configSegment,
      `Config line (${configLineIndex}) should have a mapped segment. ` +
        `Mappings: ${map.mappings}`,
    );
    assert.ok(
      helperSegment,
      `Helper line (${helperLineIndex}) should have a mapped segment. ` +
        `Mappings: ${map.mappings}`,
    );

    // Config should map to types.ts source
    assert.strictEqual(
      configSegment[1],
      typesSourceIndex,
      `Config should map to types.ts (index ${typesSourceIndex}), got index ${configSegment[1]}`,
    );

    // Helper should map to utils.ts source
    assert.strictEqual(
      helperSegment[1],
      utilsSourceIndex,
      `Helper should map to utils.ts (index ${utilsSourceIndex}), got index ${helperSegment[1]}`,
    );
  });
};
