import { strict as assert } from 'assert'
import sinon from 'sinon'

import { METADATA_STORE_KEY } from '../../../../../app/scripts/controllers/permissions/enums'

import { PermissionsController } from '../../../../../app/scripts/controllers/permissions'

import { getUserApprovalPromise, grantPermissions } from './helpers'

import {
  constants,
  getters,
  getPermControllerOpts,
  getPermissionsMiddleware,
} from './mocks'

const { CAVEATS, ERRORS, PERMS, RPC_REQUESTS } = getters

const { ACCOUNTS, DOMAINS, PERM_NAMES } = constants

const validatePermission = (perm, name, origin, caveats) => {
  assert.equal(
    name,
    perm.parentCapability,
    'should have expected permission name',
  )
  assert.equal(origin, perm.invoker, 'should have expected permission origin')
  if (caveats) {
    assert.deepEqual(
      caveats,
      perm.caveats,
      'should have expected permission caveats',
    )
  } else {
    assert.ok(!perm.caveats, 'should not have any caveats')
  }
}

const initPermController = () => {
  return new PermissionsController({
    ...getPermControllerOpts(),
  })
}

describe('permissions middleware', function () {
  describe('wallet_requestPermissions', function () {
    let permController

    beforeEach(function () {
      permController = initPermController()
      permController.notifyAccountsChanged = sinon.fake()
    })

    it('grants permissions on user approval', async function () {
      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      const req = RPC_REQUESTS.requestPermission(
        DOMAINS.a.origin,
        PERM_NAMES.eth_accounts,
      )
      const res = {}

      const userApprovalPromise = getUserApprovalPromise(permController)

      const pendingApproval = assert.doesNotReject(
        aMiddleware(req, res),
        'should not reject permissions request',
      )

      await userApprovalPromise

      assert.equal(
        permController.pendingApprovals.size,
        1,
        'perm controller should have single pending approval',
      )

      const id = permController.pendingApprovals.keys().next().value
      const approvedReq = PERMS.approvedRequest(
        id,
        PERMS.requests.eth_accounts(),
      )

      await permController.approvePermissionsRequest(
        approvedReq,
        ACCOUNTS.a.permitted,
      )
      await pendingApproval

      assert.ok(
        res.result && !res.error,
        'response should have result and no error',
      )

      assert.equal(
        res.result.length,
        1,
        'origin should have single approved permission',
      )

      validatePermission(
        res.result[0],
        PERM_NAMES.eth_accounts,
        DOMAINS.a.origin,
        CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
      )

      const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
      assert.deepEqual(
        aAccounts,
        [ACCOUNTS.a.primary],
        'origin should have correct accounts',
      )

      assert.ok(
        permController.notifyAccountsChanged.calledOnceWith(
          DOMAINS.a.origin,
          aAccounts,
        ),
        'expected notification call should have been made',
      )
    })

    it('handles serial approved requests that overwrite existing permissions', async function () {
      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      // create first request

      const req1 = RPC_REQUESTS.requestPermission(
        DOMAINS.a.origin,
        PERM_NAMES.eth_accounts,
      )
      const res1 = {}

      // send, approve, and validate first request
      // note use of ACCOUNTS.a.permitted

      let userApprovalPromise = getUserApprovalPromise(permController)

      const pendingApproval1 = assert.doesNotReject(
        aMiddleware(req1, res1),
        'should not reject permissions request',
      )

      await userApprovalPromise

      const id1 = permController.pendingApprovals.keys().next().value
      const approvedReq1 = PERMS.approvedRequest(
        id1,
        PERMS.requests.eth_accounts(),
      )

      await permController.approvePermissionsRequest(
        approvedReq1,
        ACCOUNTS.a.permitted,
      )
      await pendingApproval1

      assert.ok(
        res1.result && !res1.error,
        'response should have result and no error',
      )

      assert.equal(
        res1.result.length,
        1,
        'origin should have single approved permission',
      )

      validatePermission(
        res1.result[0],
        PERM_NAMES.eth_accounts,
        DOMAINS.a.origin,
        CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
      )

      const accounts1 = await permController.getAccounts(DOMAINS.a.origin)
      assert.deepEqual(
        accounts1,
        [ACCOUNTS.a.primary],
        'origin should have correct accounts',
      )

      assert.ok(
        permController.notifyAccountsChanged.calledOnceWith(
          DOMAINS.a.origin,
          accounts1,
        ),
        'expected notification call should have been made',
      )

      // create second request

      const requestedPerms2 = {
        ...PERMS.requests.eth_accounts(),
        ...PERMS.requests.test_method(),
      }

      const req2 = RPC_REQUESTS.requestPermissions(DOMAINS.a.origin, {
        ...requestedPerms2,
      })
      const res2 = {}

      // send, approve, and validate second request
      // note use of ACCOUNTS.b.permitted

      userApprovalPromise = getUserApprovalPromise(permController)

      const pendingApproval2 = assert.doesNotReject(
        aMiddleware(req2, res2),
        'should not reject permissions request',
      )

      await userApprovalPromise

      const id2 = permController.pendingApprovals.keys().next().value
      const approvedReq2 = PERMS.approvedRequest(id2, { ...requestedPerms2 })

      await permController.approvePermissionsRequest(
        approvedReq2,
        ACCOUNTS.b.permitted,
      )
      await pendingApproval2

      assert.ok(
        res2.result && !res2.error,
        'response should have result and no error',
      )

      assert.equal(
        res2.result.length,
        2,
        'origin should have single approved permission',
      )

      validatePermission(
        res2.result[0],
        PERM_NAMES.eth_accounts,
        DOMAINS.a.origin,
        CAVEATS.eth_accounts(ACCOUNTS.b.permitted),
      )

      validatePermission(
        res2.result[1],
        PERM_NAMES.test_method,
        DOMAINS.a.origin,
      )

      const accounts2 = await permController.getAccounts(DOMAINS.a.origin)
      assert.deepEqual(
        accounts2,
        [ACCOUNTS.b.primary],
        'origin should have correct accounts',
      )

      assert.equal(
        permController.notifyAccountsChanged.callCount,
        2,
        'should have called notification method 2 times in total',
      )

      assert.ok(
        permController.notifyAccountsChanged.lastCall.calledWith(
          DOMAINS.a.origin,
          accounts2,
        ),
        'expected notification call should have been made',
      )
    })

    it('rejects permissions on user rejection', async function () {
      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      const req = RPC_REQUESTS.requestPermission(
        DOMAINS.a.origin,
        PERM_NAMES.eth_accounts,
      )
      const res = {}

      const expectedError = ERRORS.rejectPermissionsRequest.rejection()

      const userApprovalPromise = getUserApprovalPromise(permController)

      const requestRejection = assert.rejects(
        aMiddleware(req, res),
        expectedError,
        'request should be rejected with correct error',
      )

      await userApprovalPromise

      assert.equal(
        permController.pendingApprovals.size,
        1,
        'perm controller should have single pending approval',
      )

      const id = permController.pendingApprovals.keys().next().value

      await permController.rejectPermissionsRequest(id)
      await requestRejection

      assert.ok(
        !res.result && res.error && res.error.message === expectedError.message,
        'response should have expected error and no result',
      )

      const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
      assert.deepEqual(
        aAccounts,
        [],
        'origin should have have correct accounts',
      )

      assert.ok(
        permController.notifyAccountsChanged.notCalled,
        'should not have called notification method',
      )
    })

    it('rejects requests with unknown permissions', async function () {
      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      const req = RPC_REQUESTS.requestPermissions(DOMAINS.a.origin, {
        ...PERMS.requests.does_not_exist(),
        ...PERMS.requests.test_method(),
      })
      const res = {}

      const expectedError = ERRORS.rejectPermissionsRequest.methodNotFound(
        PERM_NAMES.does_not_exist,
      )

      await assert.rejects(
        aMiddleware(req, res),
        expectedError,
        'request should be rejected with correct error',
      )

      assert.equal(
        permController.pendingApprovals.size,
        0,
        'perm controller should have no pending approvals',
      )

      assert.ok(
        !res.result && res.error && res.error.message === expectedError.message,
        'response should have expected error and no result',
      )

      assert.ok(
        permController.notifyAccountsChanged.notCalled,
        'should not have called notification method',
      )
    })

    it('accepts only a single pending permissions request per origin', async function () {
      const expectedError = ERRORS.pendingApprovals.requestAlreadyPending()

      // two middlewares for two origins

      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )
      const bMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.b.origin,
      )

      // create and start processing first request for first origin

      const reqA1 = RPC_REQUESTS.requestPermission(
        DOMAINS.a.origin,
        PERM_NAMES.test_method,
      )
      const resA1 = {}

      let userApprovalPromise = getUserApprovalPromise(permController)

      const requestApproval1 = assert.doesNotReject(
        aMiddleware(reqA1, resA1),
        'should not reject permissions request',
      )

      await userApprovalPromise

      // create and start processing first request for second origin

      const reqB1 = RPC_REQUESTS.requestPermission(
        DOMAINS.b.origin,
        PERM_NAMES.test_method,
      )
      const resB1 = {}

      userApprovalPromise = getUserApprovalPromise(permController)

      const requestApproval2 = assert.doesNotReject(
        bMiddleware(reqB1, resB1),
        'should not reject permissions request',
      )

      await userApprovalPromise

      assert.equal(
        permController.pendingApprovals.size,
        2,
        'perm controller should have expected number of pending approvals',
      )

      // create and start processing second request for first origin,
      // which should throw

      const reqA2 = RPC_REQUESTS.requestPermission(
        DOMAINS.a.origin,
        PERM_NAMES.test_method,
      )
      const resA2 = {}

      userApprovalPromise = getUserApprovalPromise(permController)

      const requestApprovalFail = assert.rejects(
        aMiddleware(reqA2, resA2),
        expectedError,
        'request should be rejected with correct error',
      )

      await userApprovalPromise
      await requestApprovalFail

      assert.ok(
        !resA2.result &&
          resA2.error &&
          resA2.error.message === expectedError.message,
        'response should have expected error and no result',
      )

      // first requests for both origins should remain

      assert.equal(
        permController.pendingApprovals.size,
        2,
        'perm controller should have expected number of pending approvals',
      )

      // now, remaining pending requests should be approved without issue

      for (const id of permController.pendingApprovals.keys()) {
        await permController.approvePermissionsRequest(
          PERMS.approvedRequest(id, PERMS.requests.test_method()),
        )
      }
      await requestApproval1
      await requestApproval2

      assert.ok(
        resA1.result && !resA1.error,
        'first response should have result and no error',
      )
      assert.equal(
        resA1.result.length,
        1,
        'first origin should have single approved permission',
      )

      assert.ok(
        resB1.result && !resB1.error,
        'second response should have result and no error',
      )
      assert.equal(
        resB1.result.length,
        1,
        'second origin should have single approved permission',
      )

      assert.equal(
        permController.pendingApprovals.size,
        0,
        'perm controller should have expected number of pending approvals',
      )
    })
  })

  describe('restricted methods', function () {
    let permController

    beforeEach(function () {
      permController = initPermController()
    })

    it('prevents restricted method access for unpermitted domain', async function () {
      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      const req = RPC_REQUESTS.test_method(DOMAINS.a.origin)
      const res = {}

      const expectedError = ERRORS.rpcCap.unauthorized()

      await assert.rejects(
        aMiddleware(req, res),
        expectedError,
        'request should be rejected with correct error',
      )

      assert.ok(
        !res.result && res.error && res.error.code === expectedError.code,
        'response should have expected error and no result',
      )
    })

    it('allows restricted method access for permitted domain', async function () {
      const bMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.b.origin,
      )

      grantPermissions(
        permController,
        DOMAINS.b.origin,
        PERMS.finalizedRequests.test_method(),
      )

      const req = RPC_REQUESTS.test_method(DOMAINS.b.origin, true)
      const res = {}

      await assert.doesNotReject(bMiddleware(req, res), 'should not reject')

      assert.ok(
        res.result && res.result === 1,
        'response should have correct result',
      )
    })
  })

  describe('eth_accounts', function () {
    let permController

    beforeEach(function () {
      permController = initPermController()
    })

    it('returns empty array for non-permitted domain', async function () {
      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin)
      const res = {}

      await assert.doesNotReject(aMiddleware(req, res), 'should not reject')

      assert.ok(
        res.result && !res.error,
        'response should have result and no error',
      )
      assert.deepEqual(res.result, [], 'response should have correct result')
    })

    it('returns correct accounts for permitted domain', async function () {
      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      grantPermissions(
        permController,
        DOMAINS.a.origin,
        PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
      )

      const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin)
      const res = {}

      await assert.doesNotReject(aMiddleware(req, res), 'should not reject')

      assert.ok(
        res.result && !res.error,
        'response should have result and no error',
      )
      assert.deepEqual(
        res.result,
        [ACCOUNTS.a.primary],
        'response should have correct result',
      )
    })
  })

  describe('eth_requestAccounts', function () {
    let permController

    beforeEach(function () {
      permController = initPermController()
    })

    it('requests accounts for unpermitted origin, and approves on user approval', async function () {
      const userApprovalPromise = getUserApprovalPromise(permController)

      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin)
      const res = {}

      const pendingApproval = assert.doesNotReject(
        aMiddleware(req, res),
        'should not reject permissions request',
      )

      await userApprovalPromise

      assert.equal(
        permController.pendingApprovals.size,
        1,
        'perm controller should have single pending approval',
      )

      const id = permController.pendingApprovals.keys().next().value
      const approvedReq = PERMS.approvedRequest(
        id,
        PERMS.requests.eth_accounts(),
      )

      await permController.approvePermissionsRequest(
        approvedReq,
        ACCOUNTS.a.permitted,
      )

      // wait for permission to be granted
      await pendingApproval

      const perms = permController.permissions.getPermissionsForDomain(
        DOMAINS.a.origin,
      )

      assert.equal(
        perms.length,
        1,
        'domain should have correct number of permissions',
      )

      validatePermission(
        perms[0],
        PERM_NAMES.eth_accounts,
        DOMAINS.a.origin,
        CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
      )

      // we should also see the accounts on the response
      assert.ok(
        res.result && !res.error,
        'response should have result and no error',
      )

      assert.deepEqual(
        res.result,
        [ACCOUNTS.a.primary],
        'result should have correct accounts',
      )

      // we should also be able to get the accounts independently
      const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
      assert.deepEqual(
        aAccounts,
        [ACCOUNTS.a.primary],
        'origin should have have correct accounts',
      )
    })

    it('requests accounts for unpermitted origin, and rejects on user rejection', async function () {
      const userApprovalPromise = getUserApprovalPromise(permController)

      const aMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.a.origin,
      )

      const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin)
      const res = {}

      const expectedError = ERRORS.rejectPermissionsRequest.rejection()

      const requestRejection = assert.rejects(
        aMiddleware(req, res),
        expectedError,
        'request should be rejected with correct error',
      )

      await userApprovalPromise

      assert.equal(
        permController.pendingApprovals.size,
        1,
        'perm controller should have single pending approval',
      )

      const id = permController.pendingApprovals.keys().next().value

      await permController.rejectPermissionsRequest(id)
      await requestRejection

      assert.ok(
        !res.result && res.error && res.error.message === expectedError.message,
        'response should have expected error and no result',
      )

      const aAccounts = await permController.getAccounts(DOMAINS.a.origin)
      assert.deepEqual(
        aAccounts,
        [],
        'origin should have have correct accounts',
      )
    })

    it('directly returns accounts for permitted domain', async function () {
      const cMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.c.origin,
      )

      grantPermissions(
        permController,
        DOMAINS.c.origin,
        PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted),
      )

      const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin)
      const res = {}

      await assert.doesNotReject(cMiddleware(req, res), 'should not reject')

      assert.ok(
        res.result && !res.error,
        'response should have result and no error',
      )
      assert.deepEqual(
        res.result,
        [ACCOUNTS.c.primary],
        'response should have correct result',
      )
    })

    it('rejects new requests when request already pending', async function () {
      let unlock
      const unlockPromise = new Promise((resolve) => {
        unlock = resolve
      })

      permController.getUnlockPromise = () => unlockPromise

      const cMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.c.origin,
      )

      grantPermissions(
        permController,
        DOMAINS.c.origin,
        PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted),
      )

      const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin)
      const res = {}

      // this will block until we resolve the unlock Promise
      const requestApproval = assert.doesNotReject(
        cMiddleware(req, res),
        'should not reject',
      )

      // this will reject because of the already pending request
      await assert.rejects(
        cMiddleware({ ...req }, {}),
        ERRORS.eth_requestAccounts.requestAlreadyPending(),
      )

      // now unlock and let through the first request
      unlock()

      await requestApproval

      assert.ok(
        res.result && !res.error,
        'response should have result and no error',
      )
      assert.deepEqual(
        res.result,
        [ACCOUNTS.c.primary],
        'response should have correct result',
      )
    })
  })

  describe('wallet_sendDomainMetadata', function () {
    let permController, clock

    beforeEach(function () {
      permController = initPermController()
      clock = sinon.useFakeTimers(1)
    })

    afterEach(function () {
      clock.restore()
    })

    it('records domain metadata', async function () {
      const name = 'BAZ'

      const cMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.c.origin,
      )

      const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
      const res = {}

      await assert.doesNotReject(cMiddleware(req, res), 'should not reject')

      assert.ok(res.result, 'result should be true')

      const metadataStore = permController.store.getState()[METADATA_STORE_KEY]

      assert.deepEqual(
        metadataStore,
        {
          [DOMAINS.c.origin]: {
            name,
            host: DOMAINS.c.host,
            lastUpdated: 1,
          },
        },
        'metadata should have been added to store',
      )
    })

    it('records domain metadata and preserves extensionId', async function () {
      const extensionId = 'fooExtension'

      const name = 'BAZ'

      const cMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.c.origin,
        extensionId,
      )

      const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
      const res = {}

      await assert.doesNotReject(cMiddleware(req, res), 'should not reject')

      assert.ok(res.result, 'result should be true')

      const metadataStore = permController.store.getState()[METADATA_STORE_KEY]

      assert.deepEqual(
        metadataStore,
        { [DOMAINS.c.origin]: { name, extensionId, lastUpdated: 1 } },
        'metadata should have been added to store',
      )
    })

    it('should not record domain metadata if no name', async function () {
      const name = null

      const cMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.c.origin,
      )

      const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
      const res = {}

      await assert.doesNotReject(cMiddleware(req, res), 'should not reject')

      assert.ok(res.result, 'result should be true')

      const metadataStore = permController.store.getState()[METADATA_STORE_KEY]

      assert.deepEqual(
        metadataStore,
        {},
        'metadata should not have been added to store',
      )
    })

    it('should not record domain metadata if no metadata', async function () {
      const cMiddleware = getPermissionsMiddleware(
        permController,
        DOMAINS.c.origin,
      )

      const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin)
      delete req.domainMetadata
      const res = {}

      await assert.doesNotReject(cMiddleware(req, res), 'should not reject')

      assert.ok(res.result, 'result should be true')

      const metadataStore = permController.store.getState()[METADATA_STORE_KEY]

      assert.deepEqual(
        metadataStore,
        {},
        'metadata should not have been added to store',
      )
    })
  })
})
