import { db } from '../../../app/src/infrastructure/mongodb.mjs'
import { expect } from 'chai'
import Settings from '@overleaf/settings'
import UserHelper from './helpers/User.mjs'
import MockHaveIBeenPwnedApiClass from './mocks/MockHaveIBeenPwnedApi.mjs'

const User = UserHelper.promises

let MockHaveIBeenPwnedApi
before(function () {
  MockHaveIBeenPwnedApi = MockHaveIBeenPwnedApiClass.instance()
})

describe('Captcha', function () {
  let user

  beforeEach('create user', async function () {
    user = new User()
    await user.ensureUserExists()
  })

  async function login(email, password, captchaResponse) {
    await user.getCsrfToken()
    return user.doRequest('POST', {
      url: '/login',
      json: {
        email,
        password,
        'g-recaptcha-response': captchaResponse,
      },
    })
  }

  async function loginWithCaptcha(captchaResponse) {
    return login(user.email, user.password, captchaResponse)
  }

  async function loginWithEmailAndCaptcha(email, captchaResponse) {
    return login(email, user.password, captchaResponse)
  }

  async function canSkipCaptcha(email) {
    await user.getCsrfToken()
    const { response, body } = await user.doRequest('POST', {
      url: '/login/can-skip-captcha',
      json: { email },
    })
    expect(response.statusCode).to.equal(200)
    return body
  }

  function expectBadCaptchaResponse(response, body) {
    expect(response.statusCode).to.equal(400)
    expect(body.errorReason).to.equal('cannot_verify_user_not_robot')
  }

  function expectSuccessfulLogin(response, body) {
    expect(response.statusCode).to.equal(200)
    expect(body).to.deep.equal({ redir: '/project' })
  }

  function expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
    response,
    body
  ) {
    expect(response.statusCode).to.equal(200)
    expect(body).to.deep.equal({ redir: '/compromised-password' })
  }

  function expectBadLogin(response, body) {
    expect(response.statusCode).to.equal(401)
    expect(body).to.deep.equal({
      message: {
        type: 'error',
        key: 'invalid-password-retry-or-reset',
      },
    })
  }

  it('should reject a login without captcha response', async function () {
    const { response, body } = await loginWithCaptcha('')
    expectBadCaptchaResponse(response, body)
  })

  it('should reject a login with an invalid captcha response', async function () {
    const { response, body } = await loginWithCaptcha('invalid')
    expectBadCaptchaResponse(response, body)
  })

  it('should accept a login with a valid captcha response', async function () {
    const { response, body } = await loginWithCaptcha('valid')
    expectSuccessfulLogin(response, body)
  })

  it('should note the solved captcha in audit log', async function () {
    const { response, body } = await loginWithCaptcha('valid')
    expectSuccessfulLogin(response, body)

    const auditLog = await user.getAuditLog()
    expect(auditLog[0].info).to.deep.equal({
      captcha: 'solved',
      method: 'Password login',
      fromKnownDevice: false,
    })
  })

  describe('trustedUsersRegex', function () {
    let resetTrustedUsersRegex
    beforeEach(function () {
      resetTrustedUsersRegex = Settings.recaptcha.trustedUsersRegex
      Settings.recaptcha.trustedUsersRegex = /\+trusted@example\.com$/
    })

    afterEach(function () {
      Settings.recaptcha.trustedUsersRegex = resetTrustedUsersRegex
    })

    describe('when user is trusted, can login without captcha', function () {
      let trustedUser
      beforeEach(async function () {
        trustedUser = new User({
          email: 'acceptance-test+trusted@example.com',
        })
        await trustedUser.ensureUserExists()
      })

      it('should be able to skip captcha', async function () {
        expect(await canSkipCaptcha(trustedUser.email)).to.equal(true)
      })

      it('should note that the user is trusted', async function () {
        const { response, body } = await login(
          trustedUser.email,
          trustedUser.password,
          ''
        )
        expectSuccessfulLogin(response, body)
        const auditLog = await trustedUser.getAuditLog()
        expect(auditLog[0].info).to.deep.equal({
          captcha: 'trusted',
          method: 'Password login',
        })
      })
    })

    describe('when user is not trusted', function () {
      it('should not be able to skip captcha', async function () {
        expect(await canSkipCaptcha(user.email)).to.equal(false)
      })

      it('should not add an audit log entry for trusted user', async function () {
        const { response, body } = await login(user.email, user.password, '')
        expectBadCaptchaResponse(response, body)
        const auditLog = await user.getAuditLog()
        expect(auditLog).to.have.lengthOf(0)
      })
    })
  })

  describe('deviceHistory', function () {
    beforeEach('login', async function () {
      const { response, body } = await loginWithCaptcha('valid')
      expectSuccessfulLogin(response, body)
    })

    it('should be able to skip captcha with the same email', async function () {
      expect(await canSkipCaptcha(user.email)).to.equal(true)
    })

    it('should be able to omit captcha with the same email', async function () {
      const { response, body } = await loginWithCaptcha('')
      expectSuccessfulLogin(response, body)
    })

    it('should note the skipped captcha in audit log', async function () {
      const { response, body } = await loginWithCaptcha('')
      expectSuccessfulLogin(response, body)

      const auditLog = await user.getAuditLog()
      expect(auditLog[1].info).to.deep.equal({
        captcha: 'skipped',
        method: 'Password login',
        fromKnownDevice: true,
      })
    })

    it('should request a captcha for another email', async function () {
      expect(await canSkipCaptcha('a@bc.de')).to.equal(false)
    })

    it('should flag missing captcha for another email', async function () {
      const { response, body } = await loginWithEmailAndCaptcha('a@bc.de', '')
      expectBadCaptchaResponse(response, body)
    })

    describe('login failure', function () {
      beforeEach(async function () {
        const { response, body } = await login(
          user.email,
          'bad password',
          'valid'
        )
        expectBadLogin(response, body)
      })

      it('should be able to skip captcha per device history', async function () {
        expect(await canSkipCaptcha(user.email)).to.equal(true)
      })

      it('should request a captcha despite device history entry', async function () {
        const { response, body } = await loginWithCaptcha('')
        expectBadCaptchaResponse(response, body)
      })

      it('should accept the login with captcha', async function () {
        const { response, body } = await loginWithCaptcha('valid')
        expectSuccessfulLogin(response, body)
      })

      describe('when the login failure happened a long time ago', function () {
        beforeEach(async function () {
          db.users.updateOne(
            { email: user.email },
            {
              $set: {
                lastFailedLogin: new Date(
                  Date.now() - 90 * 24 * 60 * 60 * 1000
                ),
              },
            }
          )
        })

        it('should be able to skip captcha per device history', async function () {
          expect(await canSkipCaptcha(user.email)).to.equal(true)
        })
        it('should accept the login without captcha', async function () {
          const { response, body } = await loginWithCaptcha('')
          expectSuccessfulLogin(response, body)
        })
        it('should accept the login with captcha', async function () {
          const { response, body } = await loginWithCaptcha('valid')
          expectSuccessfulLogin(response, body)
        })
      })
    })

    describe('cycle history', function () {
      beforeEach('create and login with 10 other users', async function () {
        for (let i = 0; i < 10; i++) {
          const otherUser = new User()
          otherUser.password = user.password
          await otherUser.ensureUserExists()
          const { response, body } = await loginWithEmailAndCaptcha(
            otherUser.email,
            'valid'
          )
          expectSuccessfulLogin(response, body)
        }
      })

      it('should have rolled out the initial users email', async function () {
        const { response, body } = await loginWithCaptcha('')
        expectBadCaptchaResponse(response, body)
      })
    })

    describe('HIBP', function () {
      before(function () {
        Settings.apis.haveIBeenPwned.enabled = true
      })
      after(function () {
        Settings.apis.haveIBeenPwned.enabled = false
      })
      beforeEach(async function () {
        user = new User()
        user.password = 'aLeakedPassword42'
        await user.ensureUserExists()
      })
      beforeEach('login to populate deviceHistory', async function () {
        const { response, body } = await loginWithCaptcha('valid')
        expectSuccessfulLogin(response, body)
      })
      beforeEach(function () {
        // echo -n aLeakedPassword42 | sha1sum
        MockHaveIBeenPwnedApi.addPasswordByHash(
          'D1ABBDEEE70CBE8BBCE5D9D039C53C0CE91C0C16'
        )
      })
      it('should be able to skip HIBP check with deviceHistory and valid captcha', async function () {
        const { response, body } = await loginWithCaptcha('valid')
        expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
          response,
          body
        )
      })

      it('should be able to skip HIBP check with deviceHistory and skipped captcha', async function () {
        const { response, body } = await loginWithCaptcha('')
        expectSuccessfulLoginWithRedirectToCompromisedPasswordPage(
          response,
          body
        )
      })

      it('should not be able to skip HIBP check without deviceHistory', async function () {
        user.resetCookies()
        const { response, body } = await loginWithCaptcha('valid')
        expect(response.statusCode).to.equal(400)
        expect(body.message.key).to.equal('password-compromised')
      })
    })
  })
})
