// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

utils.load('test/inspector/wasm-inspector-test.js');

let {session, contextGroup, Protocol} =
    InspectorTest.start('Tests stepping through wasm scripts by byte offsets');
session.setupScriptMap();

const builder = new WasmModuleBuilder();

const func_a =
    builder.addFunction('wasm_A', kSig_v_i).addBody([kExprNop, kExprNop]);
const func_a_idx = func_a.index;

// wasm_B calls wasm_A <param0> times.
const func_b = builder.addFunction('wasm_B', kSig_v_i)
    .addBody([
      // clang-format off
      kExprLoop, kWasmStmt,               // while
        kExprLocalGet, 0,                 // -
        kExprIf, kWasmStmt,               // if <param0> != 0
          kExprLocalGet, 0,               // -
          kExprI32Const, 1,               // -
          kExprI32Sub,                    // -
          kExprLocalSet, 0,               // decrease <param0>
          ...wasmI32Const(1024),          // some longer i32 const (2 byte imm)
          kExprCallFunction, func_a_idx,  // -
          kExprBr, 1,                     // continue
          kExprEnd,                       // -
        kExprEnd,                         // break
      // clang-format on
    ])
    .exportAs('main');

const module_bytes = builder.toArray();
const loop_start_offset = func_b.body_offset + 2;
const loop_body_start_offset = loop_start_offset + 2;
const loop_body_end_offset = loop_body_start_offset + 14;
const if_statement_offset = loop_body_start_offset + 2
const call_function_offset = loop_body_start_offset + 12;

const func_a_start_offset = func_a.body_offset;
const func_a_end_offset = func_a_start_offset + 2;

runTest()
    .catch(reason => InspectorTest.log(`Failed: ${reason}`))
    .then(InspectorTest.completeTest);

async function runTest() {
  await Protocol.Debugger.enable();
  InspectorTest.log('Setting up global instance variable');
  WasmInspectorTest.instantiate(module_bytes);
  const [, {params: wasmScript}] = await Protocol.Debugger.onceScriptParsed(2);
  const scriptId = wasmScript.scriptId;

  InspectorTest.log('Got wasm script: ' + wasmScript.url);

  let bpmsg = await Protocol.Debugger.setBreakpoint({
    location:
        {scriptId: scriptId, lineNumber: 0, columnNumber: loop_start_offset}
  });
  InspectorTest.logMessage(bpmsg.result.actualLocation);

  await checkValidSkipLists(scriptId);
  await checkInvalidSkipLists(scriptId);

  InspectorTest.log('Finished!');
}

async function checkValidSkipLists(scriptId) {
  InspectorTest.log('Test with valid skip lists');
  Protocol.Runtime.evaluate({expression: 'instance.exports.main(8)'});
  const {params: {callFrames}} = await Protocol.Debugger.oncePaused();
  await session.logSourceLocation(callFrames[0].location);

  InspectorTest.log('Test: Stepping over without skip list');
  let skipList = [];
  await stepThroughOneLoopIteration(skipList, 'stepOver');

  InspectorTest.log('Test: Stepping over with skip list');
  skipList = [
    createLocationRange(scriptId, loop_body_start_offset, if_statement_offset),
    createLocationRange(scriptId, call_function_offset, loop_body_end_offset)
  ];
  await stepThroughOneLoopIteration(skipList, 'stepOver');

  InspectorTest.log('Test: Stepping over start location is inclusive');
  skipList = [
    createLocationRange(
        scriptId, loop_body_start_offset, loop_body_end_offset - 1),
  ];
  await stepThroughOneLoopIteration(skipList, 'stepOver');

  InspectorTest.log('Test: Stepping over end location is exclusive');
  skipList = [
    createLocationRange(
        scriptId, loop_body_start_offset + 1, loop_body_end_offset),
  ];
  await stepThroughOneLoopIteration(skipList, 'stepOver');

  InspectorTest.log('Test: Stepping into without skip list');
  skipList = [];
  await stepThroughOneLoopIteration(skipList, 'stepInto');

  InspectorTest.log(
      'Test: Stepping into with skip list, while call itself is skipped');
  skipList = [
    createLocationRange(scriptId, func_a_start_offset, func_a_end_offset),
    createLocationRange(scriptId, if_statement_offset, loop_body_end_offset)
  ];
  await stepThroughOneLoopIteration(skipList, 'stepInto');

  InspectorTest.log('Test: Stepping into start location is inclusive');
  skipList = [
    createLocationRange(scriptId, func_a_start_offset, func_a_end_offset - 1),
  ];
  await stepThroughOneLoopIteration(skipList, 'stepInto');

  InspectorTest.log('Test: Stepping into end location is exclusive');
  skipList = [
    createLocationRange(scriptId, func_a_start_offset - 1, func_a_end_offset),
  ];
  await stepThroughOneLoopIteration(skipList, 'stepInto');

  await Protocol.Debugger.resume();
}

async function checkInvalidSkipLists(scriptId) {
  InspectorTest.log('Test with invalid skip lists');
  Protocol.Runtime.evaluate({expression: 'instance.exports.main(8)'});
  const {params: {callFrames}} = await Protocol.Debugger.oncePaused();
  await session.logSourceLocation(callFrames[0].location);

  const actions = ['stepOver', 'stepInto'];
  for (let action of actions) {
    InspectorTest.log('Test: start position has invalid column number');
    let skipList = [
      createLocationRange(scriptId, -1, loop_body_end_offset),
    ];
    await stepThroughOneLoopIteration(skipList, action);

    InspectorTest.log('Test: start position has invalid line number');
    skipList = [{
      scriptId: scriptId,
      start: {lineNumber: -1, columnNumber: 0},
      end: {lineNumber: 0, columnNumber: loop_body_end_offset}
    }];
    await stepThroughOneLoopIteration(skipList, action);

    InspectorTest.log('Test: end position smaller than start position');
    skipList = [createLocationRange(
        scriptId, loop_body_end_offset, loop_body_start_offset)];
    await stepThroughOneLoopIteration(skipList, action);

    InspectorTest.log('Test: skip list is not maximally merged');
    skipList = [
      createLocationRange(
          scriptId, loop_body_start_offset, if_statement_offset),
      createLocationRange(scriptId, if_statement_offset, loop_body_end_offset)
    ];
    await stepThroughOneLoopIteration(skipList, action);

    InspectorTest.log('Test: skip list is not sorted');
    skipList = [
      createLocationRange(scriptId, if_statement_offset, loop_body_end_offset),
      createLocationRange(
          scriptId, loop_body_start_offset, loop_body_end_offset)
    ];
    await stepThroughOneLoopIteration(skipList, action);
  }
}

async function stepThroughOneLoopIteration(skipList, stepAction) {
  InspectorTest.log(
      `Testing ${stepAction} with skipList: ${JSON.stringify(skipList)}`);
  let topFrameLocation = -1;
  while (topFrameLocation.columnNumber != loop_start_offset) {
    const stepOverMsg = await Protocol.Debugger[stepAction]({skipList});
    if (stepOverMsg.error) {
      InspectorTest.log(stepOverMsg.error.message);
      return;
    }
    const {params: {callFrames}} = await Protocol.Debugger.oncePaused();
    topFrameLocation = callFrames[0].location;
    await session.logSourceLocation(topFrameLocation);
  }
}

function createLocationRange(scriptId, startColumn, endColumn) {
  return {
    scriptId: scriptId, start: {lineNumber: 0, columnNumber: startColumn},
        end: {lineNumber: 0, columnNumber: endColumn}
  }
}