/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

/**
 * Utils for Jest
 *
 * @module utils/test-head
 */

import { combineReducers } from "devtools/client/shared/vendor/redux";
import reducers from "../reducers/index";
import actions from "../actions/index";
import * as selectors from "../selectors/index";
import {
  searchWorker,
  prettyPrintWorker,
  parserWorker,
} from "../test/tests-setup";
import configureStore from "../actions/utils/create-store";
import sourceQueue from "../utils/source-queue";
import { setupCreate } from "../client/firefox/create";
import { createLocation } from "./location";

// Import the internal module used by the source-map worker
// as node doesn't have Web Worker support and require path mapping
// doesn't work from nodejs worker thread and break mappings to devtools/ folder.
import sourceMapLoader from "devtools/client/shared/source-map-loader/source-map";

/**
 * This file contains older interfaces used by tests that have not been
 * converted to use test-mockup.js
 */

/**
 * @memberof utils/test-head
 * @static
 */
function createStore(client, initialState = {}, sourceMapLoaderMock) {
  const store = configureStore({
    log: false,
    thunkArgs: {
      client,
      sourceMapLoader:
        sourceMapLoaderMock !== undefined
          ? sourceMapLoaderMock
          : sourceMapLoader,
      parserWorker,
      prettyPrintWorker,
      searchWorker,
    },
  })(combineReducers(reducers), initialState);
  sourceQueue.clear();
  sourceQueue.initialize({
    newOriginalSources: sources =>
      store.dispatch(actions.newOriginalSources(sources)),
  });

  store.thunkArgs = () => ({
    dispatch: store.dispatch,
    getState: store.getState,
    client,
    sourceMapLoader,
    panel: {},
  });

  // Put the initial context in the store, for convenience to unit tests.
  store.cx = selectors.getThreadContext(store.getState());

  setupCreate({ store });

  return store;
}

/**
 * @memberof utils/test-head
 * @static
 */
function commonLog(msg, data = {}) {
  console.log(`[INFO] ${msg} ${JSON.stringify(data)}`);
}

function makeFrame({ id, sourceId, thread }, opts = {}) {
  const source = createSourceObject(sourceId);
  const sourceActor = {
    id: `${sourceId}-actor`,
    actor: `${sourceId}-actor`,
    source: sourceId,
    sourceObject: source,
  };
  const location = createLocation({ source, sourceActor, line: 4 });
  return {
    id,
    scope: { bindings: { variables: {}, arguments: [] } },
    location,
    generatedLocation: location,
    thread: thread || "FakeThread",
    ...opts,
  };
}

function createSourceObject(filename) {
  return {
    id: filename,
    url: makeSourceURL(filename),
    shortName: filename,
    isPrettyPrinted: false,
    isExtension: false,
    isOriginal: filename.includes("originalSource"),
    displayURL: makeSourceURL(filename),
  };
}

function makeSourceURL(filename) {
  return `http://localhost:8000/examples/${filename}`;
}

function createMakeSource() {
  const indicies = {};

  return function (name, props = {}) {
    const index = (indicies[name] | 0) + 1;
    indicies[name] = index;

    // Mock a SOURCE Resource, which happens to be the SourceActor's form
    // with resourceType and targetFront additional attributes
    return {
      resourceType: "source",
      // Mock the targetFront to support makeSourceId function
      targetFront: {
        isDestroyed() {
          return false;
        },
        getCachedFront(typeName) {
          return typeName == "thread" ? { actorID: "FakeThread" } : null;
        },
      },
      // Allow to use custom ID's for reducer source objects
      mockedJestID: name,
      actor: `${name}-${index}-actor`,
      url: `http://localhost:8000/examples/${name}`,
      sourceMapBaseURL: props.sourceMapBaseURL || null,
      sourceMapURL: props.sourceMapURL || null,
      introductionType: props.introductionType || null,
      extensionName: null,
    };
  };
}

/**
 * @memberof utils/test-head
 * @static
 */
let creator;
beforeEach(() => {
  creator = createMakeSource();
});
afterEach(() => {
  creator = null;
});
function makeSource(name, props) {
  if (!creator) {
    throw new Error("makeSource() cannot be called outside of a test");
  }

  return creator(name, props);
}

function makeOriginalSource(source) {
  return {
    id: `${source.id}/originalSource`,
    url: `${source.url}-original`,
    sourceActor: {
      id: `${source.id}-1-actor`,
      thread: "FakeThread",
    },
  };
}

function makeFuncLocation(startLine, endLine) {
  if (!endLine) {
    endLine = startLine + 1;
  }
  return {
    start: {
      line: startLine,
    },
    end: {
      line: endLine,
    },
  };
}

function makeSymbolDeclaration(name, start, end, klass) {
  return {
    id: `${name}:${start}`,
    name,
    location: makeFuncLocation(start, end),
    klass,
  };
}

/**
 * @memberof utils/test-head
 * @static
 */
function waitForState(store, predicate) {
  return new Promise(resolve => {
    let ret = predicate(store.getState());
    if (ret) {
      resolve(ret);
    }

    const unsubscribe = store.subscribe(() => {
      ret = predicate(store.getState());
      if (ret) {
        unsubscribe();
        // NOTE: memoizableAction adds an additional tick for validating context
        setTimeout(() => resolve(ret));
      }
    });
  });
}

function watchForState(store, predicate) {
  let sawState = false;
  const checkState = function () {
    if (!sawState && predicate(store.getState())) {
      sawState = true;
    }
    return sawState;
  };

  let unsubscribe;
  if (!checkState()) {
    unsubscribe = store.subscribe(() => {
      if (checkState()) {
        unsubscribe();
      }
    });
  }

  return function read() {
    if (unsubscribe) {
      unsubscribe();
    }

    return sawState;
  };
}

function getTelemetryEvents(eventName) {
  return window.dbg._telemetry.events[eventName] || [];
}

function waitATick(callback) {
  return new Promise(resolve => {
    setTimeout(() => {
      callback();
      resolve();
    });
  });
}

export {
  actions,
  selectors,
  reducers,
  createStore,
  commonLog,
  getTelemetryEvents,
  makeFrame,
  createSourceObject,
  createMakeSource,
  makeSourceURL,
  makeSource,
  makeOriginalSource,
  makeSymbolDeclaration,
  waitForState,
  watchForState,
  waitATick,
};
