const express = require('express')
const app = express()
const chai = require('chai')
const expect = chai.expect
chai.use(require('chai-http'))

// Body Parser Middleware
app.use(express.json()) // Allows us to handle raw JSON data
app.use(express.urlencoded({ extended: false })) // Allows us to handle url encoded data
const middleware = require('../../../../src/middleware/middleware')
app.use(middleware.createCtxAndReqUUID)

const idsA = []
const idsB = []
const getConstants = require('../../../../src/constants').getConstants
const errors = require('../../../../src/controller/cve-id.controller/error')
const error = new errors.CveIdControllerError()

const cveIdNonSeqFixtures = require('./mockObjects.non-sequential')
const cveIdController = require('../../../../src/controller/cve-id.controller/cve-id.controller')
const cveIdParams = require('../../../../src/controller/cve-id.controller/cve-id.middleware')

class CveIdReservePoolIncremented10IdsCaseAB1 {
  constructor () {
    this.docs = cveIdNonSeqFixtures.availableCveIdsAB
  }

  getCveIdDocuments () {
    return this.docs
  }

  async insertMany (documents) {
    documents.forEach(doc => {
      this.docs.push(doc)
    })
    return documents
  }

  async countDocuments () {
    return 0
  }

  async find (query, options) {
    const res = []
    for (let i = 0; i < this.docs.length; i++) {
      if (this.docs[i].cve_year.toString() === query.cve_year && this.docs[i].state === query.state) {
        if (res.length < options.limit) {
          res.push(this.docs[i])
        }
      }
    }

    if (res.length === 0) {
      return []
    }

    return res
  }

  async findOneAndUpdate (query, updatedCveId) {
    let index = -1
    for (let i = 0; i < this.docs.length; i++) {
      if (this.docs[i].cve_id === query.cve_id && this.docs[i].state === query.state) {
        index = i
        break
      }
    }

    if (index >= 0) {
      this.docs[index] = updatedCveId
      return this.docs[index]
    }

    return null
  }
}

class CveIdRangeReserveNonSequentialSuccessCaseAB1 {
  constructor () {
    const CONSTANTS = getConstants()

    this.year = '3000'
    this.cveIdRange = Object.assign({}, CONSTANTS.DEFAULT_CVE_ID_RANGE)
    this.cveIdRange.cve_year = this.year
    this.cveIdRange.ranges.general.top_id = 0
    this.cveIdRange.ranges.general.start = 0
    this.cveIdRange.ranges.general.end = 25
  }

  getCveIdRange () {
    return this.cveIdRange
  }

  async findOne () {
    return this.cveIdRange
  }

  async findOneAndUpdate (query, set) {
    if (query.$and !== undefined) {
      if (query.$and[0].cve_year === this.year && query.$and[1]['ranges.general.end'] !== undefined &&
      this.cveIdRange.ranges.general.end >= query.$and[1]['ranges.general.end'].$gte) {
        this.cveIdRange.ranges.general.top_id += set.$inc['ranges.general.top_id']
        return this.cveIdRange
      } else if (query.$and[0].cve_year === this.year && set.$inc['ranges.general.top_id'] !== undefined) {
        this.cveIdRange.ranges.general.top_id = set.$set['ranges.general.top_id']
        return this.cveIdRange
      }
    } else if (query.cve_year === this.year) {
      this.cveIdRange.ranges.general.top_id = set.$set['ranges.general.top_id']
      return this.cveIdRange
    }

    return null
  }
}

class CveIdReservePoolIncremented10IdsCaseAB2 {
  constructor () {
    this.docs = cveIdNonSeqFixtures.availableCveIdsAB
  }

  getCveIdDocuments () {
    return this.docs
  }

  async insertMany (documents) {
    documents.forEach(doc => {
      this.docs.push(doc)
    })
    return documents
  }

  async countDocuments () {
    return 0
  }

  async find (query, options) {
    const res = []
    for (let i = 0; i < this.docs.length; i++) {
      if (this.docs[i].cve_year.toString() === query.cve_year && this.docs[i].state === query.state) {
        if (res.length < options.limit) {
          res.push(this.docs[i])
        }
      }
    }

    if (res.length === 0) {
      return []
    }

    return res
  }

  async findOneAndUpdate (query, updatedCveId) {
    let index = -1
    for (let i = 0; i < this.docs.length; i++) {
      if (this.docs[i].cve_id === query.cve_id && this.docs[i].state === query.state) {
        index = i
        break
      }
    }

    if (index >= 0) {
      this.docs[index] = updatedCveId
      return this.docs[index]
    }

    return null
  }
}

class CveIdRangeReserveNonSequentialSuccessCaseAB2 {
  constructor () {
    const CONSTANTS = getConstants()

    this.year = '3000'
    this.cveIdRange = Object.assign({}, CONSTANTS.DEFAULT_CVE_ID_RANGE)
    this.cveIdRange.cve_year = this.year
    this.cveIdRange.ranges.general.top_id = 25
    this.cveIdRange.ranges.general.start = 0
    this.cveIdRange.ranges.general.end = 25
  }

  getCveIdRange () {
    return this.cveIdRange
  }

  async findOne () {
    return this.cveIdRange
  }

  async findOneAndUpdate (query, set) {
    if (query.$and !== undefined) {
      if (query.$and[0].cve_year === this.year && query.$and[1]['ranges.general.end'] !== undefined &&
      this.cveIdRange.ranges.general.end >= query.$and[1]['ranges.general.end'].$gte) {
        this.cveIdRange.ranges.general.top_id += set.$inc['ranges.general.top_id']
        return this.cveIdRange
      } else if (query.$and[0].cve_year === this.year && set.$inc['ranges.general.top_id'] !== undefined) {
        this.cveIdRange.ranges.general.top_id = set.$set['ranges.general.top_id']
        return this.cveIdRange
      }
    } else if (query.cve_year === this.year) {
      this.cveIdRange.ranges.general.top_id = set.$set['ranges.general.top_id']
      return this.cveIdRange
    }

    return null
  }
}

class UserReserveNonSequentialSuccessCaseAB {
  async findOneByUserNameAndOrgUUID (username) {
    if (username === cveIdNonSeqFixtures.userA.username) {
      return cveIdNonSeqFixtures.userA
    }

    return cveIdNonSeqFixtures.userB
  }
}

class OrgReserveNonSequentialSuccessCaseAB {
  async isSecretariat () {
    return false
  }

  async findOneAndUpdate (query) {
    if (query.short_name === cveIdNonSeqFixtures.orgA.short_name && cveIdNonSeqFixtures.orgA.inUse === false) {
      cveIdNonSeqFixtures.orgA.inUse = true
      return cveIdNonSeqFixtures.orgA
    } else if (query.short_name === cveIdNonSeqFixtures.orgA.short_name && cveIdNonSeqFixtures.orgA.inUse === true) {
      cveIdNonSeqFixtures.orgA.inUse = false
      return null
    } else if (query.short_name === cveIdNonSeqFixtures.orgB.short_name && cveIdNonSeqFixtures.orgB.inUse === false) {
      cveIdNonSeqFixtures.orgB.inUse = true
      return cveIdNonSeqFixtures.orgB
    } else if (query.short_name === cveIdNonSeqFixtures.orgB.short_name && cveIdNonSeqFixtures.orgB.inUse === true) {
      cveIdNonSeqFixtures.orgB.inUse = false
      return null
    }
  }

  async findOneByShortName (shortname) {
    if (shortname === cveIdNonSeqFixtures.orgA.short_name) {
      return cveIdNonSeqFixtures.orgA
    }

    return cveIdNonSeqFixtures.orgB
  }

  async getOrgUUID (shortname) {
    if (shortname === cveIdNonSeqFixtures.orgA.short_name) {
      return cveIdNonSeqFixtures.orgA.UUID
    }

    return cveIdNonSeqFixtures.orgB.UUID
  }
}

const cveIdTestRepoAB1 = new CveIdReservePoolIncremented10IdsCaseAB1()
const cveIdTestRepoAB2 = new CveIdReservePoolIncremented10IdsCaseAB2()
const cveIdRangeTestRepoAB2 = new CveIdRangeReserveNonSequentialSuccessCaseAB2()

describe('Testing the non sequential reservation (Case AB) of POST /cve-id endpoint in CveId Controller', () => {
  context('Positive Tests', () => {
    it('User A reserves 10 ids', (done) => {
      const cveIdRepo = new CveIdReservePoolIncremented10IdsCaseAB1()
      const cveIdRangeRepo = new CveIdRangeReserveNonSequentialSuccessCaseAB1()
      const userRepo = new UserReserveNonSequentialSuccessCaseAB()
      const orgRepo = new OrgReserveNonSequentialSuccessCaseAB()

      app.route('/cve-id-reserve-user-a-10-ids')
        .post((req, res, next) => {
          const factory = {
            getCveIdRepository: () => { return cveIdRepo },
            getCveIdRangeRepository: () => { return cveIdRangeRepo },
            getUserRepository: () => { return userRepo },
            getOrgRepository: () => { return orgRepo }
          }
          req.ctx.repositories = factory
          next()
        }, cveIdParams.parsePostParams, cveIdController.CVEID_RESERVE)

      chai.request(app)
        .post(`/cve-id-reserve-user-a-10-ids?short_name=${cveIdNonSeqFixtures.orgA.short_name}&cve_year=${cveIdNonSeqFixtures.year}&amount=10&batch_type=non-sequential`) // 5
        .set(cveIdNonSeqFixtures.userAHeader)
        .end((err, res) => {
          if (err) {
            done(err)
          }

          const quotaHeader = (cveIdNonSeqFixtures.orgA.policies.id_quota - 10).toString()
          expect(res.header).to.have.property('cve-api-remaining-quota').and.to.equal(quotaHeader)

          expect(res).to.have.status(200)
          expect(res).to.have.property('body').and.to.be.a('object')
          expect(res.body).to.have.property('cve_ids').and.to.be.a('array').and.to.have.lengthOf(10)

          // check that the reserved cve ids were reserved by userA
          res.body.cve_ids.forEach(obj => {
            idsA.push(obj.cve_id)
            const index = parseInt(obj.cve_id.match(/\d+$/g))
            expect(index).to.be.greaterThan(0).and.lessThan(26) // Available id range: CVE-YYYY-20001 to CVE-YYYY-20025
            expect(obj).to.have.property('state').and.to.equal('RESERVED') // checking that all CVE IDs returned are RESERVED
            expect(obj).to.have.property('owning_cna').and.to.equal(cveIdNonSeqFixtures.orgA.short_name)
            expect(obj).to.have.nested.property('requested_by.cna').and.to.equal(cveIdNonSeqFixtures.orgA.short_name)
            expect(obj).to.have.nested.property('requested_by.user').and.to.equal(cveIdNonSeqFixtures.userA.username)
          })

          // check that no duplicate cve ids were reserved
          res.body.cve_ids.forEach(id1 => {
            let counter = 0
            res.body.cve_ids.forEach(id2 => {
              if (id1 === id2) {
                counter++
              }
            })
            expect(counter).to.equal(1)
          })

          // check total count of reserved ids by userA
          const docs = cveIdTestRepoAB1.getCveIdDocuments()
          let reservedCounter = 0
          docs.forEach(doc => {
            if (doc.owning_cna === cveIdNonSeqFixtures.orgA.UUID && doc.cve_year === cveIdNonSeqFixtures.year && doc.state === 'RESERVED') {
              reservedCounter++
            }
          })
          expect(reservedCounter).to.equal(10)

          // check count of available ids
          let availableCounter = 0
          docs.forEach(doc => {
            if (doc.cve_year === cveIdNonSeqFixtures.year && doc.state === 'AVAILABLE') {
              availableCounter++
            }
          })
          expect(availableCounter).to.equal(15)

          // check that unreserved ids by userA are available
          const rangeDoc = cveIdRangeRepo.getCveIdRange()
          const topId = rangeDoc.ranges.general.top_id
          const availableDocs = []
          for (let i = 0; i < docs.length; i++) {
            let contains = false
            for (let j = 0; j < idsA.length; j++) {
              if (docs[i].cve_id === idsA[j] && docs[i].cve_year === cveIdNonSeqFixtures.year) {
                contains = true
                break
              }
            }
            if (!contains) {
              availableDocs.push(docs[i])
            }
          }
          expect(availableDocs).to.have.lengthOf(15)
          availableDocs.forEach(obj => {
            const index = parseInt(obj.cve_id.match(/\d+$/g))
            expect(index).to.be.lessThan(topId + 1) // check that the id is less than the current top id
            expect(obj).to.have.property('state').and.to.equal('AVAILABLE')
            expect(obj).to.have.property('owning_cna').and.to.equal('N/A')
            expect(obj).to.have.nested.property('requested_by.cna').and.to.equal('N/A')
            expect(obj).to.have.nested.property('requested_by.user').and.to.equal('N/A')
          })

          done()
        })
    })

    it('User B reserves 10 ids', (done) => {
      app.route('/cve-id-reserve-user-b-10-ids')
        .post((req, res, next) => {
          const factory = {
            getCveIdRepository: () => { return new CveIdReservePoolIncremented10IdsCaseAB2() },
            getCveIdRangeRepository: () => { return new CveIdRangeReserveNonSequentialSuccessCaseAB2() },
            getUserRepository: () => { return new UserReserveNonSequentialSuccessCaseAB() },
            getOrgRepository: () => { return new OrgReserveNonSequentialSuccessCaseAB() }
          }
          req.ctx.repositories = factory
          next()
        }, cveIdParams.parsePostParams, cveIdController.CVEID_RESERVE)

      chai.request(app)
        .post(`/cve-id-reserve-user-b-10-ids?short_name=${cveIdNonSeqFixtures.orgB.short_name}&cve_year=${cveIdNonSeqFixtures.year}&amount=10&batch_type=non-sequential`) // 20
        .set(cveIdNonSeqFixtures.userBHeader)
        .end((err, res) => {
          if (err) {
            done(err)
          }

          const quotaHeader = (cveIdNonSeqFixtures.orgB.policies.id_quota - 10).toString()
          expect(res.header).to.have.property('cve-api-remaining-quota').and.to.equal(quotaHeader)

          expect(res).to.have.status(200)
          expect(res).to.have.property('body').and.to.be.a('object')
          expect(res.body).to.have.property('cve_ids').and.to.be.a('array').and.to.have.lengthOf(10)

          // check that the reserved cve ids were reserved by userB
          res.body.cve_ids.forEach(obj => {
            idsB.push(obj.cve_id)
            const index = parseInt(obj.cve_id.match(/\d+$/g))
            expect(index).to.be.greaterThan(0).and.lessThan(26) // Available id range: CVE-YYYY-0001 to CVE-YYYY-0025
            expect(obj).to.have.property('state').and.to.equal('RESERVED') // checking that all CVE IDs returned are RESERVED
            expect(obj).to.have.property('owning_cna').and.to.equal(cveIdNonSeqFixtures.orgB.short_name)
            expect(obj).to.have.nested.property('requested_by.cna').and.to.equal(cveIdNonSeqFixtures.orgB.short_name)
            expect(obj).to.have.nested.property('requested_by.user').and.to.equal(cveIdNonSeqFixtures.userB.username)
          })

          // check that no duplicate cve ids were reserved
          res.body.cve_ids.forEach(id1 => {
            let counter = 0
            res.body.cve_ids.forEach(id2 => {
              if (id1 === id2) {
                counter++
              }
            })
            expect(counter).to.equal(1)
          })

          // check total count of reserved ids by userB
          const docs = cveIdTestRepoAB2.getCveIdDocuments()
          let reservedCounter = 0
          docs.forEach(doc => {
            if (doc.owning_cna === cveIdNonSeqFixtures.orgB.UUID && doc.cve_year === cveIdNonSeqFixtures.year && doc.state === 'RESERVED') {
              reservedCounter++
            }
          })
          expect(reservedCounter).to.equal(10)

          // check count of available ids
          let availableCounter = 0
          docs.forEach(doc => {
            if (doc.cve_year === cveIdNonSeqFixtures.year && doc.state === 'AVAILABLE') {
              availableCounter++
            }
          })
          expect(availableCounter).to.equal(5)

          // check that unreserved ids by userA or userB are available
          const rangeDoc = cveIdRangeTestRepoAB2.getCveIdRange()
          const topId = rangeDoc.ranges.general.top_id
          const availableDocs = []
          for (let i = 0; i < docs.length; i++) {
            let contains = false
            for (let j = 0; j < idsA.length; j++) {
              if (docs[i].cve_id === idsA[j] && docs[i].cve_year === cveIdNonSeqFixtures.year) {
                contains = true
                break
              }
            }

            if (!contains) {
              for (let j = 0; j < idsB.length; j++) {
                if (docs[i].cve_id === idsB[j] && docs[i].cve_year === cveIdNonSeqFixtures.year) {
                  contains = true
                  break
                }
              }
            }

            if (!contains) {
              availableDocs.push(docs[i])
            }
          }
          expect(availableDocs).to.have.lengthOf(5)
          availableDocs.forEach(obj => {
            const index = parseInt(obj.cve_id.match(/\d+$/g))
            expect(index).to.be.lessThan(topId + 1) // check that the id is less than the current top id
            expect(obj).to.have.property('state').and.to.equal('AVAILABLE')
            expect(obj).to.have.property('owning_cna').and.to.equal('N/A')
            expect(obj).to.have.nested.property('requested_by.cna').and.to.equal('N/A')
            expect(obj).to.have.nested.property('requested_by.user').and.to.equal('N/A')
          })

          done()
        })
    })
  })

  context('Negative Tests', () => {
    it(`User A tries to reserve 10 more ids but the CveId Range document for year ${cveIdNonSeqFixtures.year} is full`, (done) => {
      class CveIdReservePoolIncremented10IdsCaseAB3 {
        constructor () {
          this.docs = cveIdNonSeqFixtures.availableCveIdsAB
        }

        getCveIdDocuments () {
          return this.docs
        }

        async insertMany (documents) {
          documents.forEach(doc => {
            this.docs.push(doc)
          })
          return documents
        }

        async countDocuments () {
          return 10
        }

        async find (query, options) {
          const res = []
          for (let i = 0; i < this.docs.length; i++) {
            if (this.docs[i].cve_year.toString() === query.cve_year && this.docs[i].state === query.state) {
              if (res.length < options.limit) {
                res.push(this.docs[i])
              }
            }
          }

          if (res.length === 0) {
            return []
          }

          return res
        }

        async findOneAndUpdate (query, updatedCveId) {
          let index = -1
          for (let i = 0; i < this.docs.length; i++) {
            if (this.docs[i].cve_id === query.cve_id && this.docs[i].state === query.state) {
              index = i
              break
            }
          }

          if (index >= 0) {
            this.docs[index] = updatedCveId
            return this.docs[index]
          }

          return null
        }
      }

      class CveIdRangeReserveNonSequentialSuccessCaseAB3 {
        constructor () {
          const CONSTANTS = getConstants()

          this.year = '3000'
          this.cveIdRange = Object.assign({}, CONSTANTS.DEFAULT_CVE_ID_RANGE)
          this.cveIdRange.cve_year = this.year
          this.cveIdRange.ranges.general.top_id = 25
          this.cveIdRange.ranges.general.start = 0
          this.cveIdRange.ranges.general.end = 25
        }

        getCveIdRange () {
          return this.cveIdRange
        }

        async findOne () {
          return this.cveIdRange
        }

        async findOneAndUpdate (query, set) {
          if (query.$and !== undefined) {
            if (query.$and[0].cve_year === this.year && query.$and[1]['ranges.general.end'] !== undefined &&
            this.cveIdRange.ranges.general.end >= query.$and[1]['ranges.general.end'].$gte) {
              this.cveIdRange.ranges.general.top_id += set.$inc['ranges.general.top_id']
              return this.cveIdRange
            } else if (query.$and[0].cve_year === this.year && set.$inc['ranges.general.top_id'] !== undefined) {
              this.cveIdRange.ranges.general.top_id = set.$set['ranges.general.top_id']
              return this.cveIdRange
            }
          } else if (query.cve_year === this.year) {
            this.cveIdRange.ranges.general.top_id = set.$set['ranges.general.top_id']
            return this.cveIdRange
          }

          return null
        }
      }

      app.route('/cve-id-reserve-user-a-10-ids-year-full')
        .post((req, res, next) => {
          const factory = {
            getCveIdRepository: () => { return new CveIdReservePoolIncremented10IdsCaseAB3() },
            getCveIdRangeRepository: () => { return new CveIdRangeReserveNonSequentialSuccessCaseAB3() },
            getUserRepository: () => { return new UserReserveNonSequentialSuccessCaseAB() },
            getOrgRepository: () => { return new OrgReserveNonSequentialSuccessCaseAB() }
          }
          req.ctx.repositories = factory
          next()
        }, cveIdParams.parsePostParams, cveIdController.CVEID_RESERVE)

      chai.request(app)
        .post(`/cve-id-reserve-user-a-10-ids-year-full?short_name=${cveIdNonSeqFixtures.orgA.short_name}&cve_year=${cveIdNonSeqFixtures.year}&amount=10&batch_type=non-sequential`)
        .set(cveIdNonSeqFixtures.userAHeader)
        .end((err, res) => {
          if (err) {
            done(err)
          }

          const quotaHeader = (cveIdNonSeqFixtures.orgA.policies.id_quota - 10).toString()
          expect(res.header).to.have.property('cve-api-remaining-quota').and.to.equal(quotaHeader)

          expect(res).to.have.status(403)
          expect(res).to.have.property('body').and.to.be.a('object')
          const errObj = error.yearRangeFull(cveIdNonSeqFixtures.year)
          expect(res.body.error).to.equal(errObj.error)
          expect(res.body.message).to.equal(errObj.message)

          // check that no duplicate cve ids were reserved by userA and userB
          idsA.forEach(id1 => {
            let isDisjoint = true
            idsB.forEach(id2 => {
              if (id1 === id2) {
                isDisjoint = false
              }
            })
            expect(isDisjoint).to.equal(true) // pass: The intersection set between idsA and idsB is null
          })

          done()
        })
    })
  })
})
