// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {MockProtocolBackend} from '../../testing/MockScopeChain.js';
import {encodeVlqList} from '../../testing/SourceMapEncoder.js';
import {createContentProviderUISourceCode} from '../../testing/UISourceCodeHelpers.js';
import * as Bindings from '../bindings/bindings.js';
import * as SourceMapScopes from '../source_map_scopes/source_map_scopes.js';
import * as Workspace from '../workspace/workspace.js';

const {urlString} = Platform.DevToolsPath;

describeWithMockConnection('NameResolver', () => {
  const URL = urlString`file:///tmp/example.js`;
  let target: SDK.Target.Target;
  let backend: MockProtocolBackend;

  beforeEach(() => {
    const workspace = Workspace.Workspace.WorkspaceImpl.instance();
    const targetManager = SDK.TargetManager.TargetManager.instance();
    const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
    const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
      forceNew: true,
      resourceMapping,
      targetManager,
    });
    Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding});
    target = createTarget();
    backend = new MockProtocolBackend();
  });

  // Given a function scope <fn-start>,<fn-end> and a nested scope <start>,<end>,
  // we expect the scope parser to return a list of identifiers of the form [{name, offset}]
  // for the nested scope. (The nested scope may be the same as the function scope.)
  //
  // For example, say we want to assert that the block scope '{let a = x, return a}'
  // in function 'function f(x) { g(x); {let a = x, return a} }'
  //   - defines and uses variable 'a' at the correct offsets, and
  //   - uses free variable 'x'.
  // Such assertions could be expressed roughly as follows:
  //
  // expect.that(
  //  scopeIdentifiers(functionScope: {start: 10, end: 45}, scope:{start: 21, end: 43}).bound)
  //   .equals([Identifier(name: a, offsets: [27, 41])]).
  // expect.that(
  //  scopeIdentifiers(functionScope: {start: 10, end: 45}, scope:{start: 21, end: 43}).free)
  //   .equals([Identifier(name: x, offsets: [31])]).
  //
  // This is not ideal because the explicit offsets are hard to read and maintain.
  // To avoid typing the exact offset we encode the offsets in a scope assertion string
  // that can be easily aligned with the source code. For example, the assertion above
  // will be written as
  // source: 'function f(x) { g(x); {let a = x, return a} }'
  // scopes: '          {            <   B   F         B> }'
  //
  // In the assertion string, '{' and '}' characters mark the positions of function
  // offset start and end, '<' and '>' mark the positions of the nested scope
  // start and end (if '<', '>' are missing then the nested scope is the function scope),
  // the character 'B', 'F' mark the positions of bound and free identifiers that
  // we expect to be returned by the scopeIdentifiers function.
  it('test helper parses identifiers from test descriptor', () => {
    const source = 'function f(x) { g(x); {let a = x, return a} }';
    const scopes = '          {           <    B   F         B> }';
    const identifiers = getIdentifiersFromScopeDescriptor(source, scopes);
    assert.deepEqual(identifiers.bound, [
      new SourceMapScopes.NamesResolver.IdentifierPositions(
          'a', [{lineNumber: 0, columnNumber: 27}, {lineNumber: 0, columnNumber: 41}]),
    ]);
    assert.deepEqual(identifiers.free, [
      new SourceMapScopes.NamesResolver.IdentifierPositions('x', [{lineNumber: 0, columnNumber: 31}]),
    ]);
  });

  const tests = [
    {
      name: 'computes identifiers for a simple function',
      source: 'function f(x) { return x }',
      scopes: '          {B           B }',
    },
    {
      name: 'computes identifiers for a function with a let local',
      source: 'function f(x) { let a = 42; return a; }',
      scopes: '          {B        B              B  }',
    },
    {
      name: 'computes identifiers for a nested scope',
      source: 'function f(x) { let outer = x; { let inner = outer; return inner } }',
      scopes: '          {                    <     BBBBB   FFFFF         BBBBB > }',
    },
    {
      name: 'computes identifiers for second nested scope',
      source: 'function f(x) { { let a = 1; } { let b = x; return b } }',
      scopes: '          {                    <     B   F         B > }',
    },
    {
      name: 'computes identifiers with nested scopes',
      source: 'function f(x) { let outer = x; { let a = outer; } { let b = x; return b } }',
      scopes: '          {B        BBBBB   B            BBBBB              B             }',
    },
    {
      name: 'computes identifiers with nested scopes, var lifting',
      source: 'function f(x) { let outer = x; { var b = x; return b } }',
      scopes: '          {B        BBBBB   B        B   B         B   }',
    },
    {
      name: 'computes identifiers with nested scopes, var lifting',
      source: 'function f(x) { let outer = x; { var b = x; return b } }',
      scopes: '          {B        BBBBB   B        B   B         B   }',
    },
    {
      name: 'computes identifiers in catch clause',
      source: 'function f(x) { try { } catch (e) { let a = e + x; } }',
      scopes: '          {                   <B            B   F  > }',
    },
    {
      name: 'computes identifiers in catch clause',
      source: 'function f(x) { try { } catch (e) { let a = e; return a; } }',
      scopes: '          {                       <     B   F         B  > }',
    },
    {
      name: 'computes identifiers in for-let',
      source: 'function f(x) { for (let i = 0; i < 10; i++) { let j = i; console.log(j)} }',
      scopes: '          {         <    B      B       B              B  FFFFFFF       > }',
    },
    {
      name: 'computes identifiers in for-let body',
      source: 'function f(x) { for (let i = 0; i < 10; i++) { let j = i; console.log(j)} }',
      scopes: '          {                                  <     B   F  FFFFFFF     B > }',
    },
    {
      name: 'computes identifiers in for-var function',
      source: 'function f(x) { for (var i = 0; i < 10; i++) { let j = i; console.log(j)} }',
      scopes: '          {B             B      B       B              B  FFFFFFF         }',
    },
    {
      name: 'computes identifiers in for-let-of',
      source: 'function f(x) { for (let i of x) { console.log(i)} }',
      scopes: '          {         <    B    F    FFFFFFF     B > }',
    },
    {
      name: 'computes identifiers in nested arrow function',
      source: 'function f(x) { return (i) => { let j = i; return j } }',
      scopes: '          {            <B           B   B         B > }',
    },
    {
      name: 'computes identifiers in arrow function',
      source: 'const f = (x) => { let i = 1; return x + i; }',
      scopes: '          {B           B             B   B  }',
    },
    {
      name: 'computes identifiers in an arrow function\'s nested scope',
      source: 'const f = (x) => { let i = 1; { let j = i + x; return j; } }',
      scopes: '          {                   <     B   F   F         B  > }',
    },
    {
      name: 'computes identifiers in an async arrow function\'s nested scope',
      source: 'const f = async (x) => { let i = 1; { let j = i + await x; return j; } }',
      scopes: '                {                   <     B   F         F         B  > }',
    },
    {
      name: 'computes identifiers in a function with yield and await',
      source: 'async function* f(x) { return yield x + await p; }',
      scopes: '                 {B                 B         F  }',
    },
    {
      name: 'computes identifiers in a function with yield*',
      source: 'function* f(x) { return yield* g(x) + 2; }',
      scopes: '           {B                  F B       }',
    },
  ];

  const dummyMapContent = JSON.stringify({
    version: 3,
    sources: [],
  });

  for (const test of tests) {
    it(test.name, async () => {
      const callFrame = await backend.createCallFrame(
          target, {url: URL, content: test.source}, test.scopes, {url: 'file:///dummy.map', content: dummyMapContent});
      const parsedScopeChain =
          await SourceMapScopes.NamesResolver.findScopeChainForDebuggerScope(callFrame.scopeChain()[0]);
      const scope = parsedScopeChain.pop();
      assert.exists(scope);
      const identifiers =
          await SourceMapScopes.NamesResolver.scopeIdentifiers(callFrame.script, scope, parsedScopeChain);
      const boundIdentifiers = identifiers?.boundVariables ?? [];
      const freeIdentifiers = identifiers?.freeVariables ?? [];
      boundIdentifiers.sort(
          (l, r) => l.positions[0].lineNumber - r.positions[0].lineNumber ||
              l.positions[0].columnNumber - r.positions[0].columnNumber);
      freeIdentifiers.sort(
          (l, r) => l.positions[0].lineNumber - r.positions[0].lineNumber ||
              l.positions[0].columnNumber - r.positions[0].columnNumber);
      assert.deepEqual(boundIdentifiers, getIdentifiersFromScopeDescriptor(test.source, test.scopes).bound);
      assert.deepEqual(freeIdentifiers, getIdentifiersFromScopeDescriptor(test.source, test.scopes).free);
    });
  }

  it('resolves name tokens merged with commas (without source map names)', async () => {
    const sourceMapUrl = 'file:///tmp/example.js.min.map';
    // This was minified with 'esbuild --sourcemap=linked --minify' v0.14.31.
    const sourceMapContent = JSON.stringify({
      version: 3,
      sources: ['index.js'],
      sourcesContent: ['function f(par1, par2) {\n  console.log(par1, par2);\n}\nf(1, 2);\n'],
      mappings: 'AAAA,WAAW,EAAM,EAAM,CACrB,QAAQ,IAAI,EAAM,CAAI,CACxB,CACA,EAAE,EAAG,CAAC',
      names: [],
    });

    const source = `function f(o,n){console.log(o,n)}f(1,2);\n//# sourceMappingURL=${sourceMapUrl}`;
    const scopes = '          {                     }';

    const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 1}, {name: 'n', value: 2}]);
    const callFrame = await backend.createCallFrame(
        target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);

    const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]);
    const properties = await resolvedScopeObject.getAllProperties(false, false);
    const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? [];

    assert.sameDeepMembers(namesAndValues, [{name: 'par1', value: 1}, {name: 'par2', value: 2}]);
  });

  it('resolves name tokens merged with equals (without source map names)', async () => {
    const sourceMapUrl = 'file:///tmp/example.js.min.map';
    // This was minified with 'esbuild --sourcemap=linked --minify' v0.14.31.
    const sourceMapContent = JSON.stringify({
      version: 3,
      sources: ['index.js'],
      sourcesContent: ['function f(n) {\n  for (let i = 0; i < n; i++) {\n    console.log("hi");\n  }\n}\nf(10);\n'],
      mappings: 'AAAA,WAAW,EAAG,CACZ,OAAS,GAAI,EAAG,EAAI,EAAG,IACrB,QAAQ,IAAI,IAAI,CAEpB,CACA,EAAE,EAAE',
      names: [],
    });

    const source = `function f(i){for(let o=0;o<i;o++)console.log("hi")}f(10);\n//# sourceMappingURL=${sourceMapUrl}`;
    const scopes = '          {      <                                >}';

    const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 4}]);
    const callFrame = await backend.createCallFrame(
        target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);

    const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]);
    const properties = await resolvedScopeObject.getAllProperties(false, false);
    const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? [];

    assert.sameDeepMembers(namesAndValues, [{name: 'i', value: 4}]);
  });

  it('resolves name tokens with source map names', async () => {
    const sourceMapUrl = 'file:///tmp/example.js.min.map';
    // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map" --toplevel' v5.7.0.
    const sourceMapContent = JSON.stringify({
      version: 3,
      names: ['f', 'par1', 'par2', 'console', 'log'],
      sources: ['index.js'],
      sourcesContent: ['function f(par1, par2) {\n  console.log(par1, par2);\n}\nf(1, 2);\n'],
      mappings: 'AAAA,SAASA,EAAEC,EAAMC,GACfC,QAAQC,IAAIH,EAAMC,GAEpBF,EAAE,EAAG',
    });

    const source = `function o(o,n){console.log(o,n)}o(1,2);\n//# sourceMappingURL=${sourceMapUrl}`;
    const scopes = '          {                     }';

    const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 1}, {name: 'n', value: 2}]);
    const callFrame = await backend.createCallFrame(
        target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);

    const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]);
    const properties = await resolvedScopeObject.getAllProperties(false, false);
    const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? [];

    assert.sameDeepMembers(namesAndValues, [{name: 'par1', value: 1}, {name: 'par2', value: 2}]);
  });

  it('resolves names in constructors with super call', async () => {
    const sourceMapUrl = 'file:///tmp/example.js.min.map';
    // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0.
    const sourceMapContent = JSON.stringify({
      version: 3,
      names: ['C', 'B', 'constructor', 'par1', 'super', 'console', 'log'],
      sources: ['index.js'],
      mappings: 'AAAA,MAAMA,UAAUC,EACdC,YAAYC,GACVC,MAAMD,GACNE,QAAQC,IAAIH',
    });

    const source = `class C extends B{constructor(s){super(s),console.log(s)}}\n//# sourceMappingURL=${sourceMapUrl}`;
    const scopes = '                             {                          }';

    const scopeObject = backend.createSimpleRemoteObject([{name: 's', value: 42}]);
    const callFrame = await backend.createCallFrame(
        target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);

    const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]);
    const properties = await resolvedScopeObject.getAllProperties(false, false);
    const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? [];

    assert.sameDeepMembers(namesAndValues, [{name: 'par1', value: 42}]);
  });

  it('resolves names for variables in TDZ', async () => {
    const sourceMapUrl = 'file:///tmp/example.js.min.map';
    // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map" v5.7.0.
    const sourceMapContent = JSON.stringify({
      version: 3,
      names: ['adder', 'arg1', 'arg2', 'console', 'log', 'result'],
      sources: ['index.js'],
      sourcesContent: [
        'function adder(arg1, arg2) {\n  console.log(arg1, arg2);\n  const result = arg1 + arg2;\n  return result;\n}\n',
      ],
      mappings: 'AAAA,SAASA,MAAMC,EAAMC,GACnBC,QAAQC,IAAIH,EAAMC,GAClB,MAAMG,EAASJ,EAAOC,EACtB,OAAOG,CACT',
    });

    const source = `function adder(n,o){console.log(n,o);const c=n+o;return c}\n//# sourceMappingURL=${sourceMapUrl}`;
    const scopes = '              {                                          }';

    const scopeObject = backend.createSimpleRemoteObject([{name: 'n', value: 42}, {name: 'o', value: 5}, {name: 'c'}]);
    const callFrame = await backend.createCallFrame(
        target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);

    const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]);
    const properties = await resolvedScopeObject.getAllProperties(false, false);
    const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? [];

    assert.sameDeepMembers(
        namesAndValues, [{name: 'arg1', value: 42}, {name: 'arg2', value: 5}, {name: 'result', value: undefined}]);
  });

  it('resolves inner scope clashing names from let -> var transpilation', async () => {
    // This tests the  behavior where the TypeScript compiler renames a variable when transforming let-variables
    // to var-variables to avoid clash, and DevTools then (somewhat questionably) deobfuscates the var variables
    // back to the original names in the function scope (as opposed to the original block scopes). Ideally, DevTools
    // would do some scoping inference rather than relying on the pruned scope chain from V8.
    const sourceMapUrl = 'file:///tmp/index.js.map';
    // The source map was obtained with 'tsc --target es5 --sourceMap --inlineSources index.ts'.
    const sourceMapContent = JSON.stringify({
      version: 3,
      file: 'index.js',
      sourceRoot: '',
      sources: ['index.ts'],
      names: [],
      mappings: 'AAAA,SAAS,CAAC;IACR,IAAI,GAAG,GAAG,EAAE,CAAC;' +
          'IACb,KAAK,IAAI,KAAG,GAAG,CAAC,EAAE,KAAG,GAAG,CAAC,EAAE,KAAG,EAAE,EAAE;' +
          'QAChC,OAAO,CAAC,GAAG,CAAC,KAAG,CAAC,CAAC;KAClB;' +
          'AACH,CAAC;' +
          'AACD,CAAC,EAAE,CAAC',
      sourcesContent: [
        'function f() {\n  let pos = 10;\n  for (let pos = 0; pos < 5; pos++) {\n    console.log(pos);\n  }\n}\nf();\n',
      ],
    });

    const source: string[] = [];
    const scopes: string[] = [];
    source[0] = 'function f() {';
    scopes[0] = '          {';  // Mark for scope start.
    source[1] = '    var pos = 10;';
    source[2] = '    for (var pos_1 = 0; pos_1 < 5; pos_1++) {';
    source[3] = '        console.log(pos_1);';
    source[4] = '    }';
    source[5] = '}';
    scopes[5] = '}';  // Mark for scope end.
    source[6] = 'f();';
    source[7] = `//# sourceMappingURL=${sourceMapUrl}`;

    for (let i = 0; i < source.length; i++) {
      scopes[i] ??= '';
    }

    const scopeObject = backend.createSimpleRemoteObject([{name: 'pos', value: 10}, {name: 'pos_1', value: 4}]);
    const callFrame = await backend.createCallFrame(
        target, {url: URL, content: source.join('\n')}, scopes.join('\n'),
        {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);

    const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]);
    const properties = await resolvedScopeObject.getAllProperties(false, false);
    const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? [];

    assert.deepEqual(namesAndValues, [{name: 'pos', value: 10}, {name: 'pos', value: 4}]);
  });

  describe('Function name resolving', () => {
    let callFrame: SDK.DebuggerModel.CallFrame;

    beforeEach(async () => {
      const sourceMapUrl = 'file:///tmp/example.js.min.map';
      // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0.
      const sourceMapContent = JSON.stringify({
        version: 3,
        names: ['unminified', 'par1', 'par2', 'console', 'log'],
        sources: ['index.js'],
        sourcesContent: ['function unminified(par1, par2) {\n  console.log(par1, par2);\n}\n'],
        mappings: 'AAAA,SAASA,EAAWC,EAAMC,GACxBC,QAAQC,IAAIH,EAAMC',
      });

      const source = `function o(o,n){console.log(o,n)}o(1,2);\n//# sourceMappingURL=${sourceMapUrl}`;
      const scopes = '          {                     }';

      const scopeObject = backend.createSimpleRemoteObject([{name: 's', value: 42}]);
      callFrame = await backend.createCallFrame(
          target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);
    });

    it('resolves function names at scope start for a debugger frame', async () => {
      const functionName = await SourceMapScopes.NamesResolver.resolveDebuggerFrameFunctionName(callFrame);
      assert.strictEqual(functionName, 'unminified');
    });

    it('resolves function names at scope start for a profiler frame', async () => {
      const scopeLocation = callFrame.location();
      const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
      const script = debuggerModel?.scripts()[0];
      const scriptId = script?.scriptId;
      if (scriptId === undefined) {
        assert.fail('Script id not found');
        return;
      }
      const {lineNumber, columnNumber} = scopeLocation;
      await script?.requestContentData();
      const functionName = await SourceMapScopes.NamesResolver.resolveProfileFrameFunctionName(
          {scriptId, columnNumber, lineNumber}, target);
      assert.strictEqual(functionName, 'unminified');
    });
  });

  describe('Function name resolving from scopes', () => {
    it('resolves function scope name at scope start for a debugger frame', async () => {
      Root.Runtime.experiments.enableForTest('use-source-map-scopes');

      const sourceMapUrl = 'file:///tmp/example.js.min.map';
      const sourceMapContent = JSON.stringify({
        version: 3,
        names: [
          '<toplevel>',
          '<anonymous>',
          'log',
          'main',
        ],
        sources: ['main.js'],
        sourcesContent: [
          '(function () {\n  function log(m) {\n    console.log(m);\n  }\n\n  function main() {\n\t  log("hello");\n\t  log("world");\n  }\n  \n  main();\n})();',
        ],
        mappings: 'CAAA,WACE,SAAS,EAAI,GACX,QAAQ,IAAI,EACd,CAEA,SAAS,IACR,EAAI,SACJ,EAAI,QACL,CAEA,GACD,EAXD',
        x_com_bloomberg_sourcesFunctionMappings: [[
          encodeVlqList([0, 0, 0, 11, 5]),
          encodeVlqList([1, -11, 1, 11, -4]),
          encodeVlqList([1, -10, 1, 2, 2]),
          encodeVlqList([1, 2, 0, 3, 0]),
        ].join(',')],
      });

      const source = '(function(){function o(o){console.log(o)}function n(){o("hello");o("world")}n()})();\n';
      const scopes = '                                                   {                       }';

      const callFrame = await backend.createCallFrame(
          target, {url: URL, content: source + `//# sourceMappingURL=${sourceMapUrl}`}, scopes,
          {url: sourceMapUrl, content: sourceMapContent});

      const functionName = await SourceMapScopes.NamesResolver.resolveDebuggerFrameFunctionName(callFrame);
      assert.strictEqual(functionName, 'main');
      Root.Runtime.experiments.disableForTest('use-source-map-scopes');
    });
  });

  it('ignores the argument name during arrow function name resolution', async () => {
    const sourceMapUrl = 'file:///tmp/example.js.min.map';
    // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0.
    const sourceMapContent = JSON.stringify({
      version: 3,
      names: ['unminified', 'par1', 'console', 'log'],
      sources: ['index.js'],
      sourcesContent: ['const unminified = par1 => {\n  console.log(par1);\n}\n'],
      mappings: 'AAAA,MAAMA,EAAaC,IACjBC,QAAQC,IAAIF',
    });

    const source = `const o=o=>{console.log(o)};\n//# sourceMappingURL=${sourceMapUrl}`;
    const scopes = '        {                 }';

    const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 42}]);
    const callFrame = await backend.createCallFrame(
        target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]);

    assert.isNull(await SourceMapScopes.NamesResolver.resolveDebuggerFrameFunctionName(callFrame));
  });

  describe('allVariablesAtPosition', () => {
    let script: SDK.Script.Script;

    beforeEach(async () => {
      const originalContent = `
function mulWithOffset(param1, param2, offset) {
  const intermediate = param1 * param2;
  const result = intermediate;
  if (offset !== undefined) {
    const intermediate = result + offset;
    return intermediate;
  }
  return result;
}
`;
      const sourceMapUrl = 'file:///tmp/example.js.min.map';
      // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0.
      const sourceMapContent = JSON.stringify({
        version: 3,
        names: ['mulWithOffset', 'param1', 'param2', 'offset', 'intermediate', 'result', 'undefined'],
        sources: ['example.js'],
        sourcesContent: [originalContent],
        mappings:
            'AACA,SAASA,cAAcC,EAAQC,EAAQC,GACrC,MAAMC,EAAeH,EAASC,EAC9B,MAAMG,EAASD,EACf,GAAID,IAAWG,UAAW,CACxB,MAAMF,EAAeC,EAASF,EAC9B,OAAOC,CACT,CACA,OAAOC,CACT',
      });

      const scriptContent =
          'function mulWithOffset(n,t,e){const f=n*t;const u=f;if(e!==undefined){const n=u+e;return n}return u}';
      script = await backend.addScript(
          target, {url: 'file:///tmp/bundle.js', content: scriptContent},
          {url: sourceMapUrl, content: sourceMapContent});
    });

    it('has the right mapping on a function scope without shadowing', async () => {
      const location = script.rawLocation(0, 30);  // Beginning of function scope.
      assert.exists(location);

      const mapping = await SourceMapScopes.NamesResolver.allVariablesAtPosition(location);

      assert.strictEqual(mapping.get('param1'), 'n');
      assert.strictEqual(mapping.get('param2'), 't');
      assert.strictEqual(mapping.get('offset'), 'e');
      assert.strictEqual(mapping.get('intermediate'), 'f');
      assert.strictEqual(mapping.get('result'), 'u');
    });

    it('has the right mapping in a block scope with shadowing in the authored code', async () => {
      const location = script.rawLocation(0, 70);  // Beginning of block scope.
      assert.exists(location);

      const mapping = await SourceMapScopes.NamesResolver.allVariablesAtPosition(location);

      // Block scope {intermediate} shadows function scope {intermediate}.
      assert.strictEqual(mapping.get('intermediate'), 'n');
    });

    it('has the right mapping in a block scope with shadowing in the compiled code', async () => {
      const location = script.rawLocation(0, 70);  // Beginning of block scope.
      assert.exists(location);

      const mapping = await SourceMapScopes.NamesResolver.allVariablesAtPosition(location);

      assert.isNull(mapping.get('param1'));
    });
  });

  describe('getTextFor', () => {
    it('caches Text instances for scripts', async () => {
      const script = await backend.addScript(target, {url: URL, content: 'console.log(42)'}, null);

      const text1 = await SourceMapScopes.NamesResolver.getTextFor(script);
      const text2 = await SourceMapScopes.NamesResolver.getTextFor(script);

      assert.strictEqual(text1, text2);
    });

    it('caches Text instances for UISourceCodes', async () => {
      const {uiSourceCode} = createContentProviderUISourceCode(
          {target, url: URL, mimeType: 'text/typescript', content: 'console.log(42)'});

      const text1 = await SourceMapScopes.NamesResolver.getTextFor(uiSourceCode);
      const text2 = await SourceMapScopes.NamesResolver.getTextFor(uiSourceCode);

      assert.strictEqual(text1, text2);
    });
  });
});

function getIdentifiersFromScopeDescriptor(source: string, scopeDescriptor: string): {
  bound: SourceMapScopes.NamesResolver.IdentifierPositions[],
  free: SourceMapScopes.NamesResolver.IdentifierPositions[],
} {
  const bound = new Map<string, SourceMapScopes.NamesResolver.IdentifierPositions>();
  const free = new Map<string, SourceMapScopes.NamesResolver.IdentifierPositions>();
  let current = 0;

  while (current < scopeDescriptor.length) {
    while (current < scopeDescriptor.length) {
      if (scopeDescriptor[current] === 'B' || scopeDescriptor[current] === 'F') {
        break;
      }
      current++;
    }
    if (current >= scopeDescriptor.length) {
      break;
    }

    const kind = scopeDescriptor[current];
    const start = current;
    let end = start + 1;
    while (end < scopeDescriptor.length && scopeDescriptor[end] === kind) {
      end++;
    }
    if (kind === 'B') {
      addPosition(bound, start, end);
    } else {
      console.assert(kind === 'F');
      addPosition(free, start, end);
    }
    current = end + 1;
  }

  return {bound: [...bound.values()], free: [...free.values()]};

  function addPosition(
      collection: Map<string, SourceMapScopes.NamesResolver.IdentifierPositions>, start: number, end: number) {
    const name = source.substring(start, end);
    let id = collection.get(name);
    if (!id) {
      id = new SourceMapScopes.NamesResolver.IdentifierPositions(name);
      collection.set(name, id);
    }
    id.addPosition(0, start);
  }
}
