import { expect } from 'chai'
import metrics from './helpers/metrics.mjs'
import User from './helpers/User.mjs'
import redis from './helpers/redis.mjs'
import Features from '../../../app/src/infrastructure/Features.mjs'

const UserPromises = User.promises

// Expectations
async function expectProjectAccess(user, projectId) {
  // should have access to project
  await user.openProject(projectId)
}

async function expectNoProjectAccess(user, projectId) {
  // should not have access to project page
  let error = null
  try {
    await user.openProject(projectId)
  } catch (err) {
    error = err
  }
  expect(error).to.be.instanceof(Error)
}

// Actions
async function tryLoginThroughRegistrationForm(user, email, password) {
  await user.getCsrfToken()
  return await user.doRequest('POST', {
    url: '/register',
    json: { email, password },
  })
}

describe('Registration', function () {
  describe('LoginRateLimit', function () {
    let userA
    beforeEach(function () {
      userA = new UserPromises()
    })
    function loginRateLimited(line) {
      return line.includes('rate_limit_hit') && line.includes('login')
    }
    async function getLoginRateLimitHitMetricValue() {
      return await metrics.promises.getMetric(loginRateLimited)
    }
    let beforeCount
    beforeEach('get baseline metric value', async function () {
      beforeCount = await getLoginRateLimitHitMetricValue()
    })
    beforeEach('setup csrf token', async function () {
      await userA.getCsrfToken()
    })

    describe('pushing an account just below the rate limit', function () {
      async function doLoginAttempts(user, n, pushInto) {
        while (n--) {
          const { body } = await user.doRequest('POST', {
            url: '/login',
            json: {
              email: user.email,
              password: 'invalid-password',
              'g-recaptcha-response': 'valid',
            },
          })
          const message = body && body.message && body.message.key
          pushInto.push(message)
        }
      }

      let results = []
      beforeEach('do 9 login attempts', async function () {
        results = []
        await doLoginAttempts(userA, 9, results)
      })

      it('should not record any rate limited requests', async function () {
        const afterCount = await getLoginRateLimitHitMetricValue()
        expect(afterCount).to.equal(beforeCount)
      })

      it('should produce the correct responses so far', function () {
        expect(results.length).to.equal(9)
        expect(results).to.deep.equal(
          Array(9).fill('invalid-password-retry-or-reset')
        )
      })

      describe('pushing the account past the limit', function () {
        beforeEach('do 6 login attempts', async function () {
          await doLoginAttempts(userA, 6, results)
        })

        it('should record 5 rate limited requests', async function () {
          const afterCount = await getLoginRateLimitHitMetricValue()
          expect(afterCount).to.equal(beforeCount + 5)
        })

        it('should produce the correct responses', function () {
          expect(results.length).to.equal(15)
          expect(results).to.deep.equal(
            Array(10)
              .fill('invalid-password-retry-or-reset')
              .concat(Array(5).fill('too-many-login-requests-2-mins'))
          )
        })

        describe('logging in with another user', function () {
          let userB
          beforeEach(function () {
            userB = new UserPromises()
          })

          beforeEach('update baseline metric value', async function () {
            beforeCount = await getLoginRateLimitHitMetricValue()
          })
          beforeEach('setup csrf token', async function () {
            await userB.getCsrfToken()
          })

          let messages = []
          beforeEach('do bad login', async function () {
            messages = []
            await doLoginAttempts(userB, 1, messages)
          })

          it('should not rate limit their request', function () {
            expect(messages).to.deep.equal(['invalid-password-retry-or-reset'])
          })

          it('should not record any further rate limited requests', async function () {
            const afterCount = await getLoginRateLimitHitMetricValue()
            expect(afterCount).to.equal(beforeCount)
          })
        })
      })

      describe('performing a valid login for clearing the limit', function () {
        beforeEach('do login', async function () {
          await userA.login()
        })

        it('should log the user in', async function () {
          const { response } = await userA.doRequest('GET', '/project')
          expect(response.statusCode).to.equal(200)
        })

        it('should not record any rate limited requests', async function () {
          const afterCount = await getLoginRateLimitHitMetricValue()
          expect(afterCount).to.equal(beforeCount)
        })

        describe('logging out and performing more invalid login requests', function () {
          beforeEach('logout', async function () {
            await userA.logout()
          })
          beforeEach('fetch new csrf token', async function () {
            await userA.getCsrfToken()
          })

          let results = []
          beforeEach('do 9 login attempts', async function () {
            results = []
            await doLoginAttempts(userA, 9, results)
          })

          it('should not record any rate limited requests yet', async function () {
            const afterCount = await getLoginRateLimitHitMetricValue()
            expect(afterCount).to.equal(beforeCount)
          })

          it('should not emit any rate limited responses yet', function () {
            expect(results.length).to.equal(9)
            expect(results).to.deep.equal(
              Array(9).fill('invalid-password-retry-or-reset')
            )
          })
        })
      })
    })
  })

  describe('CSRF protection', function () {
    before(function () {
      if (!Features.hasFeature('registration')) {
        this.skip()
      }
    })

    beforeEach(function () {
      this.user = new UserPromises()
      this.email = `test+${Math.random()}@example.com`
      this.password = 'password11'
    })

    afterEach(async function () {
      await this.user.fullDeleteUser(this.email)
    })

    it('should register with the csrf token', async function () {
      await this.user.request.get('/login')
      await this.user.getCsrfToken()
      const { response } = await this.user.doRequest('POST', {
        url: '/register',
        json: {
          email: this.email,
          password: this.password,
        },
        headers: {
          'x-csrf-token': this.user.csrfToken,
        },
      })
      expect(response.statusCode).to.equal(200)
    })

    it('should fail with no csrf token', async function () {
      await this.user.request.get('/login')
      await this.user.getCsrfToken()
      const { response } = await this.user.doRequest('POST', {
        url: '/register',
        json: {
          email: this.email,
          password: this.password,
        },
        headers: {
          'x-csrf-token': '',
        },
      })
      expect(response.statusCode).to.equal(403)
    })

    it('should fail with a stale csrf token', async function () {
      await this.user.request.get('/login')
      await this.user.getCsrfToken()
      const oldCsrfToken = this.user.csrfToken
      await this.user.logout()
      const { response } = await this.user.doRequest('POST', {
        url: '/register',
        json: {
          email: this.email,
          password: this.password,
        },
        headers: {
          'x-csrf-token': oldCsrfToken,
        },
      })
      expect(response.statusCode).to.equal(403)
    })
  })

  describe('Register', function () {
    before(function () {
      if (!Features.hasFeature('registration')) {
        this.skip()
      }
    })

    beforeEach(function () {
      this.user = new UserPromises()
    })

    it('Set emails attribute', async function () {
      const user = await this.user.register()
      expect(user.email).to.equal(this.user.email)
      expect(user.emails).to.exist
      expect(user.emails).to.be.a('array')
      expect(user.emails.length).to.equal(1)
      expect(user.emails[0].email).to.equal(this.user.email)
    })
  })

  describe('LoginViaRegistration', function () {
    beforeEach(async function () {
      this.timeout(60000)
      this.user1 = new UserPromises()
      this.user2 = new UserPromises()

      await this.user1.login()
      await this.user1.logout()
      await redis.clearUserSessions(this.user1)
      await this.user2.login()
      await this.user2.logout()
      await redis.clearUserSessions(this.user2)

      this.project_id = null
    })

    describe('[Security] Trying to register/login as another user', function () {
      before(function () {
        if (!Features.hasFeature('registration')) {
          this.skip()
        }
      })

      it('should not allow sign in with secondary email', async function () {
        const secondaryEmail = 'acceptance-test-secondary@example.com'
        await this.user1.addEmail(secondaryEmail)
        let error = null
        try {
          await this.user1.loginWith(secondaryEmail)
        } catch (err) {
          error = err
        }
        expect(error).to.be.instanceOf(Error)
        expect(error.message).to.match(/login failed: status=401/)
        expect(error.info.body).to.deep.equal({
          message: {
            type: 'error',
            key: 'invalid-password-retry-or-reset',
          },
        })

        const isLoggedIn = await this.user1.isLoggedIn()
        expect(isLoggedIn).to.equal(false)
      })

      it('should have user1 login and create a project, which user2 cannot access', async function () {
        // user1 logs in and creates a project which only they can access
        await this.user1.login()
        const projectId = await this.user1.createProject('Private Project')
        await expectProjectAccess(this.user1, projectId)
        await expectNoProjectAccess(this.user2, projectId)
        // should prevent user2 from login/register with user1 email address
        const { body } = await tryLoginThroughRegistrationForm(
          this.user2,
          this.user1.email,
          'totally_not_the_right_password'
        )
        expect(body.redir != null).to.equal(false)
        expect(body.message != null).to.equal(true)
        expect(body.message).to.have.all.keys('type', 'text')
        expect(body.message.type).to.equal('error')
        // check user still can't access the project
        await expectNoProjectAccess(this.user2, projectId)
      })
    })
  })
})
