// tslint:disable:max-classes-per-file

import { sample } from 'lodash'

import { FactoryBot } from '../src'

import { expect, Faker } from '.'

export enum NinjaRank {
  GENIN = 'Genin',
  CHUUNIN = 'Chuunin',
  JONIN = 'Jōnin'
}

export class Ninja {
  id?: number
  name?: string
  username?: string
  level?: NinjaRank
  sensor?: boolean

  constructor(attrs?: Partial<Ninja>) {
    Object.assign(this, attrs)
  }
}

export class Village {
  id?: string
  name?: string
  members?: Array<Ninja>

  constructor(attrs?: Partial<Village>) {
    Object.assign(this, attrs)
  }
}

describe('FactoryBot', () => {
  describe('#seq', () => {
    let first

    before(() => {
      first = FactoryBot.seq(seq => `ninja${seq}@leaf.jp`)
    })

    it('generates a sequenced value for each invocation', () => {
      expect(first).to
        .eq('ninja1@leaf.jp')
    })

    context('when seq is called again', () => {
      let second

      before(() => {
        second = FactoryBot.seq(seq => `ninja${seq}@leaf.jp`)
      })

      it('does not duplicate values', () => {
        expect(first).to.not
          .eq(second)
      })
    })
  })

  describe('#rand', () => {
    let value

    context('when a Enum is given', () => {
      before(() => {
        value = FactoryBot.rand(NinjaRank)
      })

      it('returns a random enum value', () => {
        expect([NinjaRank.CHUUNIN, NinjaRank.GENIN, NinjaRank.JONIN]).to
          .include(value)
      })
    })
  })

  describe('#define', () => {
    context('when static values are given', () => {
      before(() => {
        FactoryBot.define<Ninja>('ninja', {
          id: 1,
          name: 'Uzimaki Naruto',
          username: 'naruto',
          level: NinjaRank.GENIN,
          sensor: false
        }, Ninja)
      })

      it('defines a factory with same values for each build', () => {
        expect(FactoryBot.build<Ninja>('ninja')).to.deep
          .eq(new Ninja({
            id: 1,
            name: 'Uzimaki Naruto',
            username: 'naruto',
            level: NinjaRank.GENIN,
            sensor: false
          }))
      })
    })

    context('when no model class is given', () => {
      before(() => {
        FactoryBot.define('ninja', {
          id: '1234',
          name: 'Uzimaki Naruto',
          username: 'naruto',
          level: NinjaRank.GENIN,
          sensor: false
        })
      })

      it('defines a simple factory', () => {
        expect(FactoryBot.build('ninja')).to.deep
          .eq({
            id: '1234',
            name: 'Uzimaki Naruto',
            username: 'naruto',
            level: NinjaRank.GENIN,
            sensor: false
          })
      })

      it('with no model class', () => {
        expect(FactoryBot.build('ninja')).to.not.be.an
          .instanceof(Ninja)
      })
    })

    context('when no generic type is given', () => {
      before(() => {
        FactoryBot.define('ninja', {
          id: '1234',
          name: 'Uzimaki Naruto',
          username: 'naruto',
          level: NinjaRank.GENIN,
          email: 'non-existent@property.jp'
        })
      })

      it('defines a factory with no type checking', () => {
        expect(FactoryBot.build('ninja')).to.deep
          .eq({
            id: '1234',
            name: 'Uzimaki Naruto',
            username: 'naruto',
            level: NinjaRank.GENIN,
            email: 'non-existent@property.jp'
          })
      })

      context('with function values', () => {
        before(() => {
          FactoryBot.define('ninja', {
            id: () => Faker.random.number(),
            name: () => Faker.name.findName(),
            username: () => FactoryBot.seq(seq => `${Faker.internet.userName()}_${seq}`),
            level: () => FactoryBot.rand(NinjaRank),
            sensor: () => sample([true, false])
          }, Ninja)
        })

        it('defines a factory with values that are evaluated for each build', () => {
          const first = FactoryBot.build('ninja', { name: 'Uzimaki Naruto', username: 'naruto-kun' })
          const second = FactoryBot.build('ninja', { name: 'Sasuke Uchiha', username: 'sasuke-kun' })

          expect(first).to.not.be.deep
            .eq(second)
        })
      })
    })

    context('when a chained factory is defined', () => {
      let village: Village

      before(() => {
        FactoryBot.define('ninja', {
          id: () => Faker.random.number(),
          name: () => Faker.name.findName(),
          username: () => FactoryBot.seq(seq => `${Faker.internet.userName()}_${seq}`),
          level: () => FactoryBot.rand(NinjaRank),
          sensor: () => sample([true, false])
        }, Ninja)

        FactoryBot.define('village', {
          id: () => Faker.random.uuid(),
          name: 'Leaf',
          members: () => FactoryBot.buildList('ninja', 2)
        }, Village)

        village = FactoryBot.build<Village>('village')
      })

      it('defines a factory with nested values', () => {
        expect(village.members.length).to
          .eq(2)
      })
    })
  })

  describe('#count', () => {
    context('when there are no factories defined', () => {
      before(() => {
        FactoryBot.clear()
      })

      it('returns zero', () => {
        expect(FactoryBot.count()).to
          .eq(0)
      })
    })

    context('when there are factories are defined', () => {
      before(() => {
        FactoryBot.define<Ninja>('ninja', {
          id: 1,
          name: 'Kakashi Hatake',
          username: 'kakashi',
          level: NinjaRank.GENIN,
          sensor: false
        }, Ninja)

        FactoryBot.define('village', {
          id: () => Faker.random.uuid(),
          name: 'Leaf',
          members: () => FactoryBot.buildList('ninja', 2)
        }, Village)
      })

      it('returns the quantity of defined factories', () => {
        expect(FactoryBot.count()).to
          .eq(2)
      })
    })
  })

  describe('#clear', () => {
    context('when there are factories defined', () => {
      before(() => {
        FactoryBot.define<Ninja>('ninja', {
          id: 1,
          name: 'Kakashi Hatake',
          username: 'kakashi',
          level: NinjaRank.GENIN,
          sensor: false
        }, Ninja)
      })

      it('cleans up all factories', () => {
        FactoryBot.clear()

        expect(FactoryBot.count()).to
          .eq(0)
      })
    })
  })

  describe('#extend', () => {
    context('when a factory already exist', () => {
      before(() => {
        FactoryBot.define<Ninja>('ninja', {
          id: 1,
          name: 'Kakashi Hatake',
          username: 'kakashi',
          level: NinjaRank.GENIN,
          sensor: false
        }, Ninja)

        FactoryBot.extend<Ninja>('ninja', 'jōnin', {
          level: NinjaRank.JONIN
        })

        FactoryBot.extend<Ninja>('ninja', 'chuunin:sensor', {
          level: NinjaRank.CHUUNIN,
          sensor: true
        })
      })

      it('extends existing factories in order to generate specialized data', () => {
        expect(FactoryBot.build<Ninja>('jōnin')).to.deep
          .eq(new Ninja({
            id: 1,
            name: 'Kakashi Hatake',
            username: 'kakashi',
            level: NinjaRank.JONIN,
            sensor: false
          }))

        expect(FactoryBot.build<Ninja>('chuunin:sensor')).to.deep
          .eq(new Ninja({
            id: 1,
            name: 'Kakashi Hatake',
            username: 'kakashi',
            level: NinjaRank.CHUUNIN,
            sensor: true
          }))
      })

      context('when the trait already exists', () => {
        it('throws an error', () => {
          expect(() => {
            FactoryBot.extend<Ninja>('ninja', 'jōnin', {
              level: NinjaRank.JONIN
            })
          }).to
          .throw(Error, "Factory 'ninja'`s trait 'jōnin' has already been defined!")
        })
      })
    })

    context('when the factory does not exist', () => {
      beforeEach(() => {
        FactoryBot.clear()
      })

      it('throws an error', () => {
        expect(() => {
          FactoryBot.extend<Ninja>('ninja', 'jōnin', {
            level: NinjaRank.JONIN
          })
        }).to
        .throw(Error, "Factory 'ninja' has not been defined!")
      })
    })
  })

  describe('#build', () => {
    before(() => {
      FactoryBot.define('ninja', {
        id: () => Faker.random.number(),
        name: () => Faker.name.findName(),
        username: () => FactoryBot.seq(seq => `${Faker.internet.userName()}_${seq}`),
        level: () => FactoryBot.rand(NinjaRank),
        sensor: () => sample([true, false])
      }, Ninja)
    })

    it('builds and returns a object`s instance for the given factory', () => {
      expect(FactoryBot.build<Ninja>('ninja')).to.be.an
        .instanceof(Ninja)
    })

    context('when custom parameters are given', () => {
      let ninja: Ninja

      before(() => {
        ninja = FactoryBot.build<Ninja>('ninja', {
          name: 'Uzimaki Naruto',
          username: 'naruto-kun'
        })
      })

      it('builds and returns a object`s intance with the given custom parameters', () => {
        expect(ninja).to.be.deep
          .eq(new Ninja({
            id: ninja.id,
            name: 'Uzimaki Naruto',
            username: 'naruto-kun',
            level: ninja.level,
            sensor: ninja.sensor
          }))
      })
    })
  })

  describe('#buildList', () => {
    let ninjas: Array<Ninja>

    before(() => {
      FactoryBot.define('ninja', {
        id: () => Faker.random.number(),
        name: () => Faker.name.findName(),
        username: () => FactoryBot.seq(seq => `${Faker.internet.userName()}_${seq}`),
        level: () => FactoryBot.rand(NinjaRank),
        sensor: () => sample([true, false])
      }, Ninja)

      ninjas = FactoryBot.buildList<Ninja>('ninja')
    })

    it('builds a list with at least one object`s instance', () => {
      expect(ninjas.length).to.be
        .eq(1)
    })

    context('when a length is given', () => {
      let length: number

      before(() => {
        length = 3
        ninjas = FactoryBot.buildList<Ninja>('ninja', length)
      })

      it('builds a list with the given length of object`s instances', () => {
        expect(ninjas.length).to.be
          .eq(length)
      })

      it('builds a list with different objetcs', () => {
        expect(ninjas[0].id).to.not.be
          .eq(ninjas[1].id)
      })
    })
  })
})
