// Copyright 2024 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 {encodeVlq, GeneratedRangeBuilder, OriginalScopeBuilder} from '../../testing/SourceMapEncoder.js';

import * as SDK from './sdk.js';

const {decodeOriginalScopes, decodeGeneratedRanges} = SDK.SourceMapScopes;

describe('decodeOriginalScopes', () => {
  it('throws for empty input', () => {
    assert.throws(() => decodeOriginalScopes([''], []));
  });

  it('throws for unexpected commas', () => {
    const brokenScopes = encodeVlq(42) + ',' + encodeVlq(21);
    assert.throws(() => decodeOriginalScopes([brokenScopes], []), /Unexpected char ','/);
  });

  it('throws for missing "end" item', () => {
    const names: string[] = [];
    const brokenScopes = new OriginalScopeBuilder(names).start(0, 0, {kind: 'global'}).build();
    assert.throws(() => decodeOriginalScopes([brokenScopes], names), /Malformed/);
  });

  it('throws if positions of subsequent start/end items are not monotonically increasing', () => {
    const names: string[] = [];
    const scopes = new OriginalScopeBuilder(names)
                       .start(0, 40, {kind: 'global'})
                       .start(0, 25, {kind: 'function'})
                       .end(0, 30)
                       .end(0, 50)
                       .build();
    assert.throws(() => decodeOriginalScopes([scopes], names), /Malformed/);
  });

  it('decodes scopes without "kind"', () => {
    const names: string[] = [];
    const scope = new OriginalScopeBuilder(names).start(0, 0).end(5, 0).build();

    const originalScopes = decodeOriginalScopes([scope], names);

    assert.lengthOf(originalScopes, 1);
    const {root} = originalScopes[0];

    assert.isUndefined(root.kind);
  });

  it('decodes a global scope', () => {
    const names: string[] = [];
    const scope = new OriginalScopeBuilder(names).start(0, 0, {kind: 'global'}).end(5, 0).build();

    const originalScopes = decodeOriginalScopes([scope], names);

    assert.lengthOf(originalScopes, 1);
    const {root, scopeForItemIndex} = originalScopes[0];
    assert.deepEqual(root.start, {line: 0, column: 0});
    assert.deepEqual(root.end, {line: 5, column: 0});
    assert.strictEqual(root.kind, 'global');

    assert.strictEqual(scopeForItemIndex.get(0), root);
  });

  it('ignores all but the first global scope (multiple top-level siblings)', () => {
    const names: string[] = [];
    const scope = new OriginalScopeBuilder(names)
                      .start(0, 0, {kind: 'global'})
                      .end(5, 0)
                      .start(10, 0, {kind: 'global'})
                      .end(20, 0)
                      .build();

    const originalScopes = decodeOriginalScopes([scope], names);

    assert.lengthOf(originalScopes, 1);
    const {root, scopeForItemIndex} = originalScopes[0];
    assert.deepEqual(root.start, {line: 0, column: 0});
    assert.deepEqual(root.end, {line: 5, column: 0});
    assert.lengthOf(root.children, 0);

    assert.isUndefined(scopeForItemIndex.get(2));
  });

  it('decodes nested scopes', () => {
    const names: string[] = [];
    const scope = new OriginalScopeBuilder(names)
                      .start(0, 0, {kind: 'global'})
                      .start(2, 5, {kind: 'function'})
                      .start(4, 10, {kind: 'block'})
                      .end(6, 5)
                      .end(8, 0)
                      .end(40, 0)
                      .build();

    const originalScopes = decodeOriginalScopes([scope], names);

    assert.lengthOf(originalScopes, 1);
    const {root, scopeForItemIndex} = originalScopes[0];
    assert.lengthOf(root.children, 1);
    assert.strictEqual(root.children[0].parent, root);
    assert.lengthOf(root.children[0].children, 1);
    assert.strictEqual(scopeForItemIndex.get(1), root.children[0]);

    const innerMost = root.children[0].children[0];
    assert.strictEqual(innerMost.parent, root.children[0]);
    assert.deepEqual(innerMost.start, {line: 4, column: 10});
    assert.deepEqual(innerMost.end, {line: 6, column: 5});
    assert.strictEqual(innerMost.kind, 'block');
    assert.strictEqual(scopeForItemIndex.get(2), innerMost);
  });

  it('decodes sibling scopes', () => {
    const names: string[] = [];
    const scope = new OriginalScopeBuilder(names)
                      .start(0, 0, {kind: 'global'})
                      .start(2, 5, {kind: 'function'})
                      .end(4, 0)
                      .start(6, 6, {kind: 'function'})
                      .end(8, 0)
                      .end(10, 0)
                      .build();

    const originalScopes = decodeOriginalScopes([scope], names);

    assert.lengthOf(originalScopes, 1);
    const {root, scopeForItemIndex} = originalScopes[0];
    assert.lengthOf(root.children, 2);
    assert.strictEqual(root.children[0].kind, 'function');
    assert.strictEqual(root.children[1].kind, 'function');

    assert.strictEqual(scopeForItemIndex.get(1), root.children[0]);
    assert.strictEqual(scopeForItemIndex.get(3), root.children[1]);

    assert.strictEqual(root.children[0].parent, root);
    assert.strictEqual(root.children[1].parent, root);
  });

  it('decodes scope names', () => {
    const names: string[] = [];
    const scope = new OriginalScopeBuilder(names)
                      .start(0, 0, {kind: 'global'})
                      .start(2, 5, {kind: 'class', name: 'FooClass'})
                      .start(4, 10, {kind: 'function', name: 'fooMethod'})
                      .end(6, 5)
                      .end(8, 0)
                      .end(40, 0)
                      .build();

    const originalScopes = decodeOriginalScopes([scope], names);

    assert.lengthOf(originalScopes, 1);
    const {root} = originalScopes[0];
    assert.lengthOf(root.children, 1);
    assert.strictEqual(root.children[0].name, 'FooClass');

    assert.lengthOf(root.children[0].children, 1);
    assert.strictEqual(root.children[0].children[0].name, 'fooMethod');
  });

  it('decodes variable names', () => {
    const names: string[] = [];
    const scope = new OriginalScopeBuilder(names)
                      .start(0, 0, {kind: 'global'})
                      .start(2, 5, {kind: 'function', name: 'fooFunction', variables: ['functionVarFoo']})
                      .start(4, 10, {kind: 'block', variables: ['blockVarFoo', 'blockVarBar']})
                      .end(6, 5)
                      .end(8, 0)
                      .end(40, 0)
                      .build();

    const originalScopes = decodeOriginalScopes([scope], names);

    assert.lengthOf(originalScopes, 1);
    const {root} = originalScopes[0];
    assert.lengthOf(root.children, 1);
    assert.deepEqual(root.children[0].variables, ['functionVarFoo']);

    assert.lengthOf(root.children[0].children, 1);
    assert.deepEqual(root.children[0].children[0].variables, ['blockVarFoo', 'blockVarBar']);
  });
});

describe('decodeGeneratedRanges', () => {
  it('returns an empty array for empty input', () => {
    assert.lengthOf(decodeGeneratedRanges('', [], []), 0);
  });

  it('throws for missing "end" item', () => {
    const brokenRanges = new GeneratedRangeBuilder([]).start(0, 0).build();
    assert.throws(() => decodeGeneratedRanges(brokenRanges, [], []), /Malformed/);
  });

  it('decodes a single range', () => {
    const range = new GeneratedRangeBuilder([]).start(0, 0).end(5, 0).build();

    const [generatedRange] = decodeGeneratedRanges(range, [], []);

    assert.deepEqual(generatedRange.start, {line: 0, column: 0});
    assert.deepEqual(generatedRange.end, {line: 5, column: 0});
  });

  it('decodes multiple top-level ranges', () => {
    const range = new GeneratedRangeBuilder([]).start(0, 0).end(0, 10).start(0, 11).end(0, 20).build();

    const [generatedRange1, generatedRange2] = decodeGeneratedRanges(range, [], []);

    assert.deepEqual(generatedRange1.start, {line: 0, column: 0});
    assert.deepEqual(generatedRange1.end, {line: 0, column: 10});

    assert.deepEqual(generatedRange2.start, {line: 0, column: 11});
    assert.deepEqual(generatedRange2.end, {line: 0, column: 20});
  });

  it('decodes a nested range on a single line', () => {
    const range = new GeneratedRangeBuilder([]).start(0, 0).start(0, 5).end(0, 10).end(0, 15).build();

    const [generatedRange] = decodeGeneratedRanges(range, [], []);

    assert.deepEqual(generatedRange.start, {line: 0, column: 0});
    assert.deepEqual(generatedRange.end, {line: 0, column: 15});

    assert.lengthOf(generatedRange.children, 1);
    assert.deepEqual(generatedRange.children[0].start, {line: 0, column: 5});
    assert.deepEqual(generatedRange.children[0].end, {line: 0, column: 10});
  });

  it('decodes sibling ranges on a single line', () => {
    const range =
        new GeneratedRangeBuilder([]).start(0, 0).start(0, 5).end(0, 10).start(0, 15).end(0, 20).end(0, 25).build();

    const [generatedRange] = decodeGeneratedRanges(range, [], []);

    assert.deepEqual(generatedRange.start, {line: 0, column: 0});
    assert.deepEqual(generatedRange.end, {line: 0, column: 25});

    assert.lengthOf(generatedRange.children, 2);
    assert.deepEqual(generatedRange.children[0].start, {line: 0, column: 5});
    assert.deepEqual(generatedRange.children[0].end, {line: 0, column: 10});
    assert.deepEqual(generatedRange.children[1].start, {line: 0, column: 15});
    assert.deepEqual(generatedRange.children[1].end, {line: 0, column: 20});
  });

  it('decodes nested and sibling ranges over multiple lines', () => {
    const range = new GeneratedRangeBuilder([])
                      .start(0, 0)
                      .start(5, 0)
                      .start(10, 8)
                      .end(15, 4)
                      .end(20, 0)
                      .start(25, 4)
                      .end(30, 0)
                      .end(35, 0)
                      .build();

    const [generatedRange] = decodeGeneratedRanges(range, [], []);

    assert.deepEqual(generatedRange.start, {line: 0, column: 0});
    assert.deepEqual(generatedRange.end, {line: 35, column: 0});

    assert.lengthOf(generatedRange.children, 2);
    assert.deepEqual(generatedRange.children[0].start, {line: 5, column: 0});
    assert.deepEqual(generatedRange.children[0].end, {line: 20, column: 0});

    assert.lengthOf(generatedRange.children[0].children, 1);
    assert.deepEqual(generatedRange.children[0].children[0].start, {line: 10, column: 8});
    assert.deepEqual(generatedRange.children[0].children[0].end, {line: 15, column: 4});

    assert.deepEqual(generatedRange.children[1].start, {line: 25, column: 4});
    assert.deepEqual(generatedRange.children[1].end, {line: 30, column: 0});
  });

  it('throws if the definition references has an invalid source index', () => {
    const names: string[] = [];
    const originEncodedScopes = new OriginalScopeBuilder(names).start(2, 0, {kind: 'function'}).end(5, 0).build();
    const originalScopes = decodeOriginalScopes([originEncodedScopes], names);
    const range =
        new GeneratedRangeBuilder([]).start(0, 0, {definition: {sourceIdx: 1, scopeIdx: 0}}).end(0, 20).build();

    assert.throws(() => decodeGeneratedRanges(range, originalScopes, []), /Invalid source index/);
  });

  it('throws if the definition references has an invalid scope index', () => {
    const names: string[] = [];
    const originEncodedScopes = new OriginalScopeBuilder(names).start(2, 0, {kind: 'function'}).end(5, 0).build();
    const originalScopes = decodeOriginalScopes([originEncodedScopes], names);
    const range =
        new GeneratedRangeBuilder([]).start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 4}}).end(0, 20).build();

    assert.throws(() => decodeGeneratedRanges(range, originalScopes, []), /Invalid original scope index/);
  });

  it('decodes original scope (definition) references', () => {
    const names: string[] = [];
    const originEncodedScopes = new OriginalScopeBuilder(names)
                                    .start(0, 0, {kind: 'global'})
                                    .start(5, 0, {kind: 'function'})
                                    .end(10, 0)
                                    .end(20, 0)
                                    .build();
    const originalScopes = decodeOriginalScopes([originEncodedScopes], names);
    const range = new GeneratedRangeBuilder([])
                      .start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
                      .start(0, 5, {definition: {sourceIdx: 0, scopeIdx: 1}})
                      .end(0, 10)
                      .end(0, 20)
                      .build();

    const [generatedRange] = decodeGeneratedRanges(range, originalScopes, []);

    assert.strictEqual(generatedRange.originalScope, originalScopes[0].root, 'range does not reference global scope');
    assert.strictEqual(
        generatedRange.children[0].originalScope, originalScopes[0].root.children[0],
        'range does not reference function scope');
  });

  it('decodes original scope (definition) references across multiple original sources', () => {
    const names: string[] = [];
    const originEncodedScopes1 = new OriginalScopeBuilder(names)
                                     .start(0, 0, {kind: 'global'})
                                     .start(5, 0, {kind: 'function'})
                                     .end(10, 0)
                                     .end(20, 0)
                                     .build();
    const originEncodedScopes2 = new OriginalScopeBuilder(names)
                                     .start(0, 0, {kind: 'global'})
                                     .start(5, 0, {kind: 'function'})
                                     .end(10, 0)
                                     .end(20, 0)
                                     .build();
    const originalScopes = decodeOriginalScopes([originEncodedScopes1, originEncodedScopes2], names);
    const range = new GeneratedRangeBuilder([])
                      .start(0, 0)
                      .start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 1}})
                      .end(0, 5)
                      .start(0, 10, {definition: {sourceIdx: 1, scopeIdx: 1}})
                      .end(0, 15)
                      .end(0, 16)
                      .build();

    const [generatedRange] = decodeGeneratedRanges(range, originalScopes, []);

    assert.strictEqual(generatedRange.children[0].originalScope, originalScopes[0].root.children[0]);
    assert.strictEqual(generatedRange.children[1].originalScope, originalScopes[1].root.children[0]);
  });

  it('throws if an inlined range\'s callsite references an invalid source index', () => {
    const names: string[] = [];
    const originEncodedScopes = new OriginalScopeBuilder(names).start(2, 0, {kind: 'function'}).end(5, 0).build();
    const originalScopes = decodeOriginalScopes([originEncodedScopes], names);
    const range =
        new GeneratedRangeBuilder([]).start(0, 0, {callsite: {sourceIdx: 1, line: 0, column: 0}}).end(0, 20).build();

    assert.throws(() => decodeGeneratedRanges(range, originalScopes, []), /Invalid source index/);
  });

  it('decodes multiple callsite references in the same source file and the same line', () => {
    const names: string[] = [];
    const originEncodedScopes = new OriginalScopeBuilder(names)
                                    .start(0, 0, {kind: 'global'})
                                    .start(1, 0, {kind: 'function'})
                                    .end(4, 0)
                                    .end(10, 0)
                                    .build();
    const originalScopes = decodeOriginalScopes([originEncodedScopes], names);
    const range =
        new GeneratedRangeBuilder([])
            .start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
            .start(0, 5, {definition: {sourceIdx: 0, scopeIdx: 1}, callsite: {sourceIdx: 0, line: 6, column: 0}})
            .end(0, 7)
            .start(0, 8, {definition: {sourceIdx: 0, scopeIdx: 1}, callsite: {sourceIdx: 0, line: 8, column: 5}})
            .end(0, 12)
            .start(0, 13, {definition: {sourceIdx: 0, scopeIdx: 1}, callsite: {sourceIdx: 0, line: 8, column: 15}})
            .end(0, 18)
            .end(0, 20)
            .build();

    const [generatedRange] = decodeGeneratedRanges(range, originalScopes, []);

    assert.lengthOf(generatedRange.children, 3);
    assert.deepEqual(generatedRange.children[0].callsite, {sourceIndex: 0, line: 6, column: 0});
    assert.deepEqual(generatedRange.children[1].callsite, {sourceIndex: 0, line: 8, column: 5});
    assert.deepEqual(generatedRange.children[2].callsite, {sourceIndex: 0, line: 8, column: 15});
  });

  it('decodes multiple callsite refrences over multiple source files', () => {
    // A single function in the first file, is called in the first and second file. The bundler inlines both call-sites.
    const names: string[] = [];
    const originalEncodedScopes1 = new OriginalScopeBuilder(names)
                                       .start(0, 0, {kind: 'global'})
                                       .start(1, 0, {kind: 'function'})
                                       .end(4, 0)
                                       .end(10, 0)
                                       .build();
    const originalEncodedScopes2 = new OriginalScopeBuilder(names).start(0, 0, {kind: 'global'}).end(10, 0).build();
    const originalScopes = decodeOriginalScopes([originalEncodedScopes1, originalEncodedScopes2], names);
    const range =
        new GeneratedRangeBuilder([])
            .start(
                0, 0)  // Pseudo root range so we can have multiple global ranges. This will be fixed soon in the spec.
            .start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
            .start(5, 0, {definition: {sourceIdx: 0, scopeIdx: 1}, callsite: {sourceIdx: 0, line: 7, column: 5}})
            .end(8, 0)
            .end(20, 0)
            .start(21, 0, {definition: {sourceIdx: 1, scopeIdx: 0}})
            .start(22, 0, {definition: {sourceIdx: 0, scopeIdx: 1}, callsite: {sourceIdx: 1, line: 3, column: 7}})
            .end(25, 0)
            .end(40, 0)
            .end(40, 0)
            .build();

    const [generatedRange] = decodeGeneratedRanges(range, originalScopes, []);

    assert.lengthOf(generatedRange.children, 2);
    assert.lengthOf(generatedRange.children[0].children, 1);
    assert.deepEqual(generatedRange.children[0].children[0].callsite, {sourceIndex: 0, line: 7, column: 5});

    assert.lengthOf(generatedRange.children[1].children, 1);
    assert.deepEqual(generatedRange.children[1].children[0].callsite, {sourceIndex: 1, line: 3, column: 7});
  });

  it('decodes bindings that are available/unavailable for the full range', () => {
    const names: string[] = [];
    const originalEncodedScopes = new OriginalScopeBuilder(names)
                                      .start(0, 0, {kind: 'global', variables: ['foo', 'bar', 'baz']})
                                      .end(10, 0)
                                      .build();
    const originalScopes = decodeOriginalScopes([originalEncodedScopes], names);
    const range = new GeneratedRangeBuilder(names)
                      .start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}, bindings: ['x', undefined, 'y']})
                      .end(0, 100)
                      .build();

    const [generatedRange] = decodeGeneratedRanges(range, originalScopes, names);

    assert.lengthOf(generatedRange.values, 3);
    assert.deepEqual(generatedRange.values, ['x', undefined, 'y']);
  });

  it('decodes bindings that are available with different expressions throughout a range', () => {
    const names: string[] = [];
    const originalEncodedScopes =
        new OriginalScopeBuilder(names).start(0, 0, {kind: 'global', variables: ['foo']}).end(10, 0).build();
    const originalScopes = decodeOriginalScopes([originalEncodedScopes], names);
    const range = new GeneratedRangeBuilder(names)
                      .start(0, 0, {
                        definition: {sourceIdx: 0, scopeIdx: 0},
                        bindings: [[
                          {line: 0, column: 0, name: 'x'},
                          {line: 0, column: 30, name: undefined},
                          {line: 0, column: 60, name: 'y'},
                        ]],
                      })
                      .end(0, 100)
                      .build();

    const [generatedRange] = decodeGeneratedRanges(range, originalScopes, names);

    assert.lengthOf(generatedRange.values, 1);
    assert.deepEqual(generatedRange.values[0], [
      {from: {line: 0, column: 0}, to: {line: 0, column: 30}, value: 'x'},
      {from: {line: 0, column: 30}, to: {line: 0, column: 60}, value: undefined},
      {from: {line: 0, column: 60}, to: {line: 0, column: 100}, value: 'y'},
    ]);
  });

  it('decodes the "isStackFrame" flag in original scopes', () => {
    const names: string[] = [];
    const originalEncodedScopes = new OriginalScopeBuilder(names).start(0, 0, {isStackFrame: true}).end(5, 0).build();

    const [{root}] = decodeOriginalScopes([originalEncodedScopes], names);

    assert.isTrue(root.isStackFrame);
  });

  it('decodes the "isStackFrame" flag in generated ranges', () => {
    const range = new GeneratedRangeBuilder([])
                      .start(0, 0)
                      .start(5, 0, {isStackFrame: true})
                      .end(10, 0)
                      .start(20, 4, {isStackFrame: false})
                      .end(30, 0)
                      .end(40, 0)
                      .build();

    const [generatedRange] = decodeGeneratedRanges(range, [], []);
    assert.lengthOf(generatedRange.children, 2);
    assert.isTrue(generatedRange.children[0].isStackFrame);
    assert.isFalse(generatedRange.children[1].isStackFrame);
  });

  it('decodes the "isHidden" flag', () => {
    const range = new GeneratedRangeBuilder([])
                      .start(0, 0)
                      .start(5, 0, {isHidden: true})
                      .end(10, 0)
                      .start(20, 4)
                      .end(30, 0)
                      .end(40, 0)
                      .build();

    const [generatedRange] = decodeGeneratedRanges(range, [], []);
    assert.lengthOf(generatedRange.children, 2);
    assert.isTrue(generatedRange.children[0].isHidden);
    assert.isFalse(generatedRange.children[1].isHidden);
  });
});
