import { strict as assert } from 'assert';
import sinon from 'sinon';
import nock from 'nock';
import { ObservableStore } from '@metamask/obs-store';
import BigNumber from 'bignumber.js';
import { ControllerMessenger } from '@metamask/base-controller';
import {
  TokenListController,
  TokensController,
  AssetsContractController,
} from '@metamask/assets-controllers';
import { toHex } from '@metamask/controller-utils';
import { NetworkController } from '@metamask/network-controller';
import { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens';
import PreferencesController from './preferences';

function buildMessenger() {
  return new ControllerMessenger().getRestricted({
    name: 'DetectTokensController',
    allowedEvents: ['NetworkController:stateChange'],
  });
}

describe('DetectTokensController', function () {
  let sandbox,
    assetsContractController,
    keyringMemStore,
    network,
    preferences,
    provider,
    tokensController,
    tokenListController;

  const noop = () => undefined;

  const networkControllerProviderConfig = {
    getAccounts: noop,
  };

  const infuraProjectId = 'infura-project-id';

  beforeEach(async function () {
    sandbox = sinon.createSandbox();
    // Disable all requests, even those to localhost
    nock.disableNetConnect();
    nock('https://mainnet.infura.io')
      .post(`/v3/${infuraProjectId}`)
      .reply(200, (_uri, requestBody) => {
        if (requestBody.method === 'eth_getBlockByNumber') {
          return {
            id: requestBody.id,
            jsonrpc: '2.0',
            result: {
              number: '0x42',
            },
          };
        }

        if (requestBody.method === 'eth_blockNumber') {
          return {
            id: requestBody.id,
            jsonrpc: '2.0',
            result: '0x42',
          };
        }

        throw new Error(`(Infura) Mock not defined for ${requestBody.method}`);
      })
      .persist();
    nock('https://sepolia.infura.io')
      .post(`/v3/${infuraProjectId}`)
      .reply(200, (_uri, requestBody) => {
        if (requestBody.method === 'eth_getBlockByNumber') {
          return {
            id: requestBody.id,
            jsonrpc: '2.0',
            result: {
              number: '0x42',
            },
          };
        }

        if (requestBody.method === 'eth_blockNumber') {
          return {
            id: requestBody.id,
            jsonrpc: '2.0',
            result: '0x42',
          };
        }

        throw new Error(`(Infura) Mock not defined for ${requestBody.method}`);
      })
      .persist();
    nock('http://localhost:8545')
      .post('/')
      .reply(200, (_uri, requestBody) => {
        if (requestBody.method === 'eth_getBlockByNumber') {
          return {
            id: requestBody.id,
            jsonrpc: '2.0',
            result: {
              number: '0x42',
            },
          };
        }

        if (requestBody.method === 'eth_blockNumber') {
          return {
            id: requestBody.id,
            jsonrpc: '2.0',
            result: '0x42',
          };
        }

        if (requestBody.method === 'net_version') {
          return {
            id: requestBody.id,
            jsonrpc: '2.0',
            result: '1337',
          };
        }

        throw new Error(
          `(localhost) Mock not defined for ${requestBody.method}`,
        );
      })
      .persist();
    nock('https://token-api.metaswap.codefi.network')
      .get(`/tokens/1`)
      .reply(200, [
        {
          address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
          symbol: 'SNX',
          decimals: 18,
          occurrences: 11,
          aggregators: [
            'paraswap',
            'pmm',
            'airswapLight',
            'zeroEx',
            'bancor',
            'coinGecko',
            'zapper',
            'kleros',
            'zerion',
            'cmc',
            'oneInch',
          ],
          name: 'Synthetix',
          iconUrl: 'https://airswap-token-images.s3.amazonaws.com/SNX.png',
        },
        {
          address: '0x514910771af9ca656af840dff83e8264ecf986ca',
          symbol: 'LINK',
          decimals: 18,
          occurrences: 11,
          aggregators: [
            'paraswap',
            'pmm',
            'airswapLight',
            'zeroEx',
            'bancor',
            'coinGecko',
            'zapper',
            'kleros',
            'zerion',
            'cmc',
            'oneInch',
          ],
          name: 'Chainlink',
          iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png',
        },
        {
          address: '0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c',
          symbol: 'BNT',
          decimals: 18,
          occurrences: 11,
          aggregators: [
            'paraswap',
            'pmm',
            'airswapLight',
            'zeroEx',
            'bancor',
            'coinGecko',
            'zapper',
            'kleros',
            'zerion',
            'cmc',
            'oneInch',
          ],
          name: 'Bancor',
          iconUrl: 'https://s3.amazonaws.com/airswap-token-images/BNT.png',
        },
      ])
      .get(`/tokens/3`)
      .reply(200, { error: 'ChainId 3 is not supported' })
      .persist();

    keyringMemStore = new ObservableStore({ isUnlocked: false });
    const networkControllerMessenger = new ControllerMessenger();
    network = new NetworkController({
      messenger: networkControllerMessenger,
      infuraProjectId,
    });
    await network.initializeProvider(networkControllerProviderConfig);
    provider = network.getProviderAndBlockTracker().provider;

    const tokenListMessenger = new ControllerMessenger().getRestricted({
      name: 'TokenListController',
    });
    tokenListController = new TokenListController({
      chainId: toHex(1),
      preventPollingOnNetworkRestart: false,
      onNetworkStateChange: sinon.spy(),
      onPreferencesStateChange: sinon.spy(),
      messenger: tokenListMessenger,
    });
    await tokenListController.start();

    preferences = new PreferencesController({
      network,
      provider,
      tokenListController,
      onInfuraIsBlocked: sinon.stub(),
      onInfuraIsUnblocked: sinon.stub(),
    });
    preferences.setAddresses([
      '0x7e57e2',
      '0xbc86727e770de68b1060c91f6bb6945c73e10388',
    ]);
    preferences.setUseTokenDetection(true);

    tokensController = new TokensController({
      config: { provider },
      onPreferencesStateChange: preferences.store.subscribe.bind(
        preferences.store,
      ),
      onNetworkStateChange: networkControllerMessenger.subscribe.bind(
        networkControllerMessenger,
        'NetworkController:stateChange',
      ),
    });

    assetsContractController = new AssetsContractController({
      onPreferencesStateChange: preferences.store.subscribe.bind(
        preferences.store,
      ),
      onNetworkStateChange: networkControllerMessenger.subscribe.bind(
        networkControllerMessenger,
        'NetworkController:stateChange',
      ),
    });
  });

  afterEach(function () {
    nock.enableNetConnect('localhost');
    sandbox.restore();
  });

  it('should poll on correct interval', async function () {
    const stub = sinon.stub(global, 'setInterval');
    new DetectTokensController({ messenger: buildMessenger(), interval: 1337 }); // eslint-disable-line no-new
    assert.strictEqual(stub.getCall(0).args[1], 1337);
    stub.restore();
  });

  it('should be called on every polling period', async function () {
    const clock = sandbox.useFakeTimers();
    await network.setProviderType(NETWORK_TYPES.MAINNET);
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokenList: tokenListController,
      tokensController,
      assetsContractController,
    });
    controller.isOpen = true;
    controller.isUnlocked = true;

    const stub = sandbox.stub(controller, 'detectNewTokens');

    clock.tick(1);
    sandbox.assert.notCalled(stub);
    clock.tick(180000);
    sandbox.assert.called(stub);
    clock.tick(180000);
    sandbox.assert.calledTwice(stub);
    clock.tick(180000);
    sandbox.assert.calledThrice(stub);
  });

  it('should not check and add tokens while on unsupported networks', async function () {
    sandbox.useFakeTimers();
    await network.setProviderType(NETWORK_TYPES.SEPOLIA);
    const tokenListMessengerSepolia = new ControllerMessenger().getRestricted({
      name: 'TokenListController',
    });
    tokenListController = new TokenListController({
      chainId: toHex(11155111),
      onNetworkStateChange: sinon.spy(),
      onPreferencesStateChange: sinon.spy(),
      messenger: tokenListMessengerSepolia,
    });
    await tokenListController.start();
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokenList: tokenListController,
      tokensController,
      assetsContractController,
    });
    controller.isOpen = true;
    controller.isUnlocked = true;

    const stub = sandbox.stub(
      assetsContractController,
      'getBalancesInSingleCall',
    );

    await controller.detectNewTokens();
    sandbox.assert.notCalled(stub);
  });

  it('should skip adding tokens listed in ignoredTokens array', async function () {
    sandbox.useFakeTimers();
    await network.setProviderType(NETWORK_TYPES.MAINNET);
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokenList: tokenListController,
      tokensController,
      assetsContractController,
      trackMetaMetricsEvent: noop,
    });
    controller.isOpen = true;
    controller.isUnlocked = true;

    const { tokenList } = tokenListController.state;
    const tokenValues = Object.values(tokenList);

    await tokensController.addDetectedTokens([
      {
        address: tokenValues[0].address,
        symbol: tokenValues[0].symbol,
        decimals: tokenValues[0].decimals,
        aggregators: undefined,
        image: undefined,
        isERC721: undefined,
      },
    ]);

    sandbox
      .stub(assetsContractController, 'getBalancesInSingleCall')
      .callsFake((tokensToDetect) =>
        tokensToDetect.map((token) =>
          token.address === tokenValues[1].address ? new BigNumber(10) : 0,
        ),
      );
    await tokensController.ignoreTokens([tokenValues[1].address]);

    await controller.detectNewTokens();
    assert.deepEqual(tokensController.state.detectedTokens, [
      {
        address: toChecksumHexAddress(tokenValues[0].address),
        decimals: tokenValues[0].decimals,
        symbol: tokenValues[0].symbol,
        aggregators: undefined,
        image: undefined,
        isERC721: undefined,
      },
    ]);
  });

  it('should check and add tokens while on supported networks', async function () {
    sandbox.useFakeTimers();
    await network.setProviderType(NETWORK_TYPES.MAINNET);
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokenList: tokenListController,
      tokensController,
      assetsContractController,
      trackMetaMetricsEvent: noop,
    });
    controller.isOpen = true;
    controller.isUnlocked = true;

    const { tokenList } = tokenListController.state;
    const erc20ContractAddresses = Object.keys(tokenList);

    const existingTokenAddress = erc20ContractAddresses[0];
    const existingToken = tokenList[existingTokenAddress];

    await tokensController.addDetectedTokens([
      {
        address: existingToken.address,
        symbol: existingToken.symbol,
        decimals: existingToken.decimals,
        aggregators: undefined,
        image: undefined,
        isERC721: undefined,
      },
    ]);
    const tokenAddressToAdd = erc20ContractAddresses[1];
    const tokenToAdd = tokenList[tokenAddressToAdd];
    sandbox
      .stub(assetsContractController, 'getBalancesInSingleCall')
      .callsFake(() =>
        Promise.resolve({ [tokenAddressToAdd]: new BigNumber(10) }),
      );
    await controller.detectNewTokens();
    assert.deepEqual(tokensController.state.detectedTokens, [
      {
        address: toChecksumHexAddress(existingTokenAddress),
        decimals: existingToken.decimals,
        symbol: existingToken.symbol,
        aggregators: undefined,
        image: undefined,
        isERC721: undefined,
      },
      {
        address: toChecksumHexAddress(tokenAddressToAdd),
        decimals: tokenToAdd.decimals,
        symbol: tokenToAdd.symbol,
        aggregators: undefined,
        image: undefined,
        isERC721: undefined,
      },
    ]);
  });

  it('should trigger detect new tokens when change address', async function () {
    sandbox.useFakeTimers();
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokenList: tokenListController,
      tokensController,
      assetsContractController,
    });
    controller.isOpen = true;
    controller.isUnlocked = true;
    const stub = sandbox.stub(controller, 'detectNewTokens');
    await preferences.setSelectedAddress(
      '0xbc86727e770de68b1060c91f6bb6945c73e10388',
    );
    sandbox.assert.called(stub);
  });

  it('should trigger detect new tokens when submit password', async function () {
    sandbox.useFakeTimers();
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokenList: tokenListController,
      tokensController,
      assetsContractController,
    });
    controller.isOpen = true;
    controller.selectedAddress = '0x0';
    const stub = sandbox.stub(controller, 'detectNewTokens');
    await controller._keyringMemStore.updateState({ isUnlocked: true });
    sandbox.assert.called(stub);
  });

  it('should not trigger detect new tokens when not unlocked', async function () {
    const clock = sandbox.useFakeTimers();
    await network.setProviderType(NETWORK_TYPES.MAINNET);
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokenList: tokenListController,
      tokensController,
      assetsContractController,
    });
    controller.isOpen = true;
    controller.isUnlocked = false;
    const stub = sandbox.stub(
      assetsContractController,
      'getBalancesInSingleCall',
    );
    clock.tick(180000);
    sandbox.assert.notCalled(stub);
  });

  it('should not trigger detect new tokens when not open', async function () {
    const clock = sandbox.useFakeTimers();
    await network.setProviderType(NETWORK_TYPES.MAINNET);
    const controller = new DetectTokensController({
      messenger: buildMessenger(),
      preferences,
      network,
      keyringMemStore,
      tokensController,
      assetsContractController,
    });
    // trigger state update from preferences controller
    await preferences.setSelectedAddress(
      '0xbc86727e770de68b1060c91f6bb6945c73e10388',
    );
    controller.isOpen = false;
    controller.isUnlocked = true;
    const stub = sandbox.stub(
      assetsContractController,
      'getBalancesInSingleCall',
    );
    clock.tick(180000);
    sandbox.assert.notCalled(stub);
  });
});
