/* -*- Mode: js; js-indent-level: 2; -*- */
/*
 * Copyright 2024 Mozilla Foundation and contributors
 * Licensed under the New BSD license. See LICENSE or:
 * http://opensource.org/licenses/BSD-3-Clause
 */

const fs = require("fs").promises;
const SourceMapConsumer =
  require("../lib/source-map-consumer").SourceMapConsumer;

const sourceMapSpecTests = require("./source-map-tests/source-map-spec-tests.json");

async function readJSON(path) {
  const file = await fs.open(require.resolve(path));
  const json = JSON.parse(await file.readFile());
  file.close();
  return json;
}

// Known failures due to intentional implementation choices or due to bugs.
const skippedTests = [
  // Versions are explicitly checked a bit loosely.
  "versionNumericString",
  // Stricter sources array checking isn't implemented.
  "sourcesNotStringOrNull",
  "sourcesAndSourcesContentBothNull",
  // Stricter names array checking isn't implemented.
  "namesMissing",
  "namesNotString",
  // This check isn't as strict in this library.
  "invalidMappingNotAString1",
  // A mapping segment with no fields is technically invalid in the spec.
  "invalidMappingSegmentWithZeroFields",
  // These tests fail due to imprecision in the spec about the 32-bit limit.
  "invalidMappingSegmentWithColumnExceeding32Bits",
  "invalidMappingSegmentWithOriginalLineExceeding32Bits",
  "invalidMappingSegmentWithOriginalColumnExceeding32Bits",
  // A large VLQ that should parse, but currently does not.
  "validMappingLargeVLQ",
  // The library currently doesn't check the types of offset lines/columns.
  "indexMapOffsetLineWrongType",
  "indexMapOffsetColumnWrongType",
  // The spec is not totally clear about this case.
  "indexMapInvalidBaseMappings",
  // The spec's definition of overlap can be refined
  "indexMapInvalidOverlap",
  // The library doesn't support the new ignoreList feature yet.
  "ignoreListWrongType1",
  "ignoreListWrongType2",
  "ignoreListWrongType3",
  "ignoreListOutOfBounds",
];

// The source-map library converts null sources to the "null" URL in its
// sources list, so for equality checking we accept this as null.
function nullish(nullOrString) {
  if (nullOrString === "null") {
    return null;
  }
  return nullOrString;
}

function mapLine(line) {
  return line + 1;
}

async function testMappingAction(assert, rawSourceMap, action) {
  return SourceMapConsumer.with(rawSourceMap, null, consumer => {
    let mappedPosition = consumer.originalPositionFor({
      line: mapLine(action.generatedLine),
      column: action.generatedColumn,
    });

    assert.equal(
      mappedPosition.line,
      mapLine(action.originalLine),
      `original line didn't match, expected ${mapLine(
        action.originalLine
      )} got ${mappedPosition.line}`
    );
    assert.equal(
      mappedPosition.column,
      action.originalColumn,
      `original column didn't match, expected ${action.originalColumn} got ${mappedPosition.column}`
    );
    assert.equal(
      nullish(mappedPosition.source),
      action.originalSource,
      `original source didn't match, expected ${action.originalSource} got ${mappedPosition.source}`
    );
    if (action.mappedName) {
      assert.equal(
        mappedPosition.name,
        action.mappedName,
        `mapped name didn't match, expected ${action.mappedName} got ${mappedPosition.name}`
      );
    }

    // When the source is null, a reverse lookup may not make sense
    // because there isn't a unique way to look it up.
    if (action.originalSource !== null) {
      mappedPosition = consumer.generatedPositionFor({
        source: action.originalSource,
        line: mapLine(action.originalLine),
        column: action.originalColumn,
      });

      assert.equal(
        mappedPosition.line,
        mapLine(action.generatedLine),
        `generated line didn't match, expected ${mapLine(
          action.generatedLine
        )} got ${mappedPosition.line}`
      );
      assert.equal(
        mappedPosition.column,
        action.generatedColumn,
        `generated column didn't match, expected ${action.generatedColumn} got ${mappedPosition.column}`
      );
    }
  });
}

async function testTransitiveMappingAction(assert, rawSourceMap, action) {
  return SourceMapConsumer.with(rawSourceMap, null, async consumer => {
    assert.ok(
      Array.isArray(action.intermediateMaps),
      "transitive mapping case requires intermediate maps"
    );

    let mappedPosition = consumer.originalPositionFor({
      line: mapLine(action.generatedLine),
      column: action.generatedColumn,
    });

    for (const intermediateMapPath of action.intermediateMaps) {
      const intermediateMap = await readJSON(
        `./source-map-tests/resources/${intermediateMapPath}`
      );
      await SourceMapConsumer.with(
        intermediateMap,
        null,
        consumerIntermediate => {
          mappedPosition = consumerIntermediate.originalPositionFor({
            line: mappedPosition.line,
            column: mappedPosition.column,
          });
        }
      );
    }

    assert.equal(
      mappedPosition.line,
      mapLine(action.originalLine),
      `original line didn't match, expected ${mapLine(
        action.originalLine
      )} got ${mappedPosition.line}`
    );
    assert.equal(
      mappedPosition.column,
      action.originalColumn,
      `original column didn't match, expected ${action.originalColumn} got ${mappedPosition.column}`
    );
    assert.equal(
      mappedPosition.source,
      action.originalSource,
      `original source didn't match, expected ${action.originalSource} got ${mappedPosition.source}`
    );
  });
}

for (const testCase of sourceMapSpecTests.tests) {
  if (skippedTests.includes(testCase.name)) {
    continue;
  }
  exports[`test from source map spec tests, name: ${testCase.name}`] =
    async function (assert) {
      const json = await readJSON(
        `./source-map-tests/resources/${testCase.sourceMapFile}`
      );
      try {
        const map = await new SourceMapConsumer(json);
        map.eachMapping(() => {});
        map.destroy();
      } catch (exn) {
        if (testCase.sourceMapIsValid) {
          assert.fail(
            "Expected valid source map but failed to load successfully: " +
              exn.message
          );
        }
        return;
      }
      if (!testCase.sourceMapIsValid) {
        assert.fail("Expected invalid source map but loaded successfully");
      }
      if (testCase.testActions) {
        for (const testAction of testCase.testActions) {
          if (testAction.actionType == "checkMapping") {
            await testMappingAction(assert, json, testAction);
          } else if (testAction.actionType == "checkMappingTransitive") {
            await testTransitiveMappingAction(assert, json, testAction);
          }
        }
      }
    };
}
