// Copyright 2023 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 SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Workspace from '../../models/workspace/workspace.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {MockProtocolBackend} from '../../testing/MockScopeChain.js';
import {getInitializedResourceTreeModel} from '../../testing/ResourceTreeHelpers.js';
import {createContentProviderUISourceCode} from '../../testing/UISourceCodeHelpers.js';

import * as Coverage from './coverage.js';

const {urlString} = Platform.DevToolsPath;
const {CoverageDecorationManager} = Coverage.CoverageDecorationManager;

/** Test helper that returns the "identity" line ranges for any given string */
function lineRangesForContent(content: string): TextUtils.TextRange.TextRange[] {
  const ranges: TextUtils.TextRange.TextRange[] = [];
  const text = new TextUtils.Text.Text(content);
  for (let i = 0; i < text.lineCount(); ++i) {
    const line = text.lineAt(i);
    ranges.push(new TextUtils.TextRange.TextRange(i, 0, i, line.length));
  }
  return ranges;
}

describeWithMockConnection('CoverageDeocrationManager', () => {
  let target: SDK.Target.Target;
  let backend: MockProtocolBackend;
  let debuggerBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding;
  let workspace: Workspace.Workspace.WorkspaceImpl;
  let cssBinding: Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding;
  let coverageModel: sinon.SinonStubbedInstance<Coverage.CoverageModel.CoverageModel>;

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

    // Since we wanna mock 'usageForRange' we stub the whole instance. Otherwise we'd use half
    // a stub and half the real thing.
    coverageModel = sinon.createStubInstance(Coverage.CoverageModel.CoverageModel);

    // Wait for the resource tree model to load; otherwise, our uiSourceCodes could be asynchronously
    // invalidated during the test.
    await getInitializedResourceTreeModel(target);
  });

  const URL = urlString`http://example.com/index.js`;

  describe('usageByLine (raw)', () => {
    it('marks lines as "unknown" coverge status if no coverage info is available', async () => {
      await backend.addScript(target, {url: URL, content: 'function foo(a,b){return a+b;}'}, null);
      const uiSourceCode = workspace.uiSourceCodeForURL(URL);
      assert.exists(uiSourceCode);
      await uiSourceCode.requestContentData();
      const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);

      const usage = await manager.usageByLine(uiSourceCode, lineRangesForContent(uiSourceCode.content()));

      assert.deepEqual(usage, [undefined]);
    });

    it('marks lines as covered if coverage info says so', async () => {
      await backend.addScript(target, {url: URL, content: 'function foo(a,b){return a+b;}'}, null);
      const uiSourceCode = workspace.uiSourceCodeForURL(URL);
      assert.exists(uiSourceCode);
      await uiSourceCode.requestContentData();
      coverageModel.usageForRange.returns(true);
      const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);

      const usage = await manager.usageByLine(uiSourceCode, lineRangesForContent(uiSourceCode.content()));
      assert.deepEqual(usage, [true]);
    });
  });

  describe('usageByLine (formatted)', () => {
    it('marks lines as covered if coverage info says so', async () => {
      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}';
      const script = await backend.addScript(target, {url: URL, content: scriptContent}, null);
      const uiSourceCode = workspace.uiSourceCodeForURL(URL);
      assert.exists(uiSourceCode);
      await uiSourceCode.requestContentData();
      coverageModel.usageForRange.callsFake((contentProvider, startOffset, endOffset) => {
        assert.strictEqual(contentProvider, script);
        // Everything is covered except the body of the `if`.
        return endOffset <= 70 || startOffset > 90;
      });
      const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);

      // clang-format off
      // Simulate editor pretty-printing `script`.
      const lineRanges = [
        new TextUtils.TextRange.TextRange(0, 0, 0, 30),    // function mulWithOffset(n,t,e){
        new TextUtils.TextRange.TextRange(0, 30, 0, 42),   //   const f=n*t;
        new TextUtils.TextRange.TextRange(0, 42, 0, 52),   //   const u=f;
        new TextUtils.TextRange.TextRange(0, 52, 0, 70),   //   if(e!==undefined){
        new TextUtils.TextRange.TextRange(0, 70, 0, 82),   //     const n=u+e;
        new TextUtils.TextRange.TextRange(0, 82, 0, 90),   //     return n
        new TextUtils.TextRange.TextRange(0, 90, 0, 91),   //   }
        new TextUtils.TextRange.TextRange(0, 91, 0, 99),   //   return u
        new TextUtils.TextRange.TextRange(0, 99, 0, 100),  // }
      ];
      // clang-format on
      const usage = await manager.usageByLine(uiSourceCode, lineRanges);

      assert.deepEqual(usage, [true, true, true, true, false, false, false, true, true]);
    });
  });

  describe('usageByLine (sourcemap)', () => {
    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('marks lines as covered if coverage info says so', async () => {
      const uiSourceCode = workspace.uiSourceCodeForURL(urlString`file:///tmp/example.js`);
      assert.exists(uiSourceCode);
      await uiSourceCode.requestContentData();
      coverageModel.usageForRange.callsFake((contentProvider, startOffset, endOffset) => {
        assert.strictEqual(contentProvider, script);
        // Everything is covered except the body of the `if`.
        return endOffset < 70 || startOffset > 90;
      });
      const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);

      const usage = await manager.usageByLine(uiSourceCode, lineRangesForContent(uiSourceCode.content()));
      assert.deepEqual(usage, [undefined, true, true, true, true, false, false, undefined, true, undefined, undefined]);
    });
  });

  it('sets the "decorationData" on all existing UISourceCodes', () => {
    const {uiSourceCode} = createContentProviderUISourceCode({url: URL, mimeType: 'text/javascript'});
    const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);

    assert.strictEqual(uiSourceCode.getDecorationData(Coverage.CoverageDecorationManager.decoratorType), manager);
  });

  it('sets the "decorationData" on newly added UISourceCodes (after the manager already exists)', () => {
    const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);
    const {uiSourceCode} = createContentProviderUISourceCode({url: URL, mimeType: 'text/javascript'});

    assert.strictEqual(uiSourceCode.getDecorationData(Coverage.CoverageDecorationManager.decoratorType), manager);
  });

  it('does not update the "decorationData" on newly added UISourceCodes after being disposed', () => {
    const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);
    manager.dispose();

    const {uiSourceCode} = createContentProviderUISourceCode({url: URL, mimeType: 'text/javascript'});

    assert.isUndefined(uiSourceCode.getDecorationData(Coverage.CoverageDecorationManager.decoratorType));
  });

  describe('reset', () => {
    it('resets the "decorationData" on all existing UISourceCodes to "undefined"', () => {
      const {uiSourceCode} = createContentProviderUISourceCode({url: URL, mimeType: 'text/javascript'});
      const manager = new CoverageDecorationManager(coverageModel, workspace, debuggerBinding, cssBinding);

      manager.reset();

      assert.isUndefined(uiSourceCode.getDecorationData(Coverage.CoverageDecorationManager.decoratorType));
    });
  });
});
