import { it, describe } from 'mocha'
import { expect } from 'chai'
// import Deneric, { DenericSchema } from '../dist/deneric'
import Deneric, { DenericSchema } from '../src/Deneric'

const STUDENT_SCHEMA: DenericSchema = {
  full_name: ['profile.full_name', String],
  age: ['profile.age', Number],
  is_male: ['others.is_male', Boolean],
  roles: ['others.roles', Deneric.Array(String)],
  jobs: ['jobs', Deneric.Map(Deneric.Array(String))],
  highscores: ['highscores', Deneric.Array(Number)]
}

class Student extends Deneric {
  full_name: string = 'noname'
  age: number = -1
  is_male: boolean = false
  roles: (string | number)[] = ['100']
  jobs: { [key: string]: string[] } = { 2023: ['2023'] }
  highscores!: number[]

  constructor(data: any) {
    super(STUDENT_SCHEMA)
    this.fromJson(data)
  }
}

class StudentIgnoreJob extends Deneric {
  full_name: string = 'noname'
  age: number = -1
  is_male: boolean = false
  roles: (string | number)[] = ['100']
  jobs: { [key: string]: string[] } = { 2023: ['2023'] }

  constructor(data: any) {
    super({
      full_name: ['profile.full_name', String],
      age: ['profile.age', Number],
      is_male: ['others.is_male', Boolean],
      roles: ['others.roles', Deneric.Array(String)],
      jobs: ['jobs', Object, true],
    })
    this.fromJson(data)
  }
}


class ClassRoom extends Deneric {
  monitor: Student = new Student({})
  students: Student[] = []
  mapStudents: Record<string, Student> = {}

  constructor() {
    super({
      monitor: ['class_monitor', Student],
      students: ['my_student', Deneric.Array(Student)],
      mapStudents: ['map_student', Deneric.Map(Student)]
    })
  }
}

class InvalidStudent extends Deneric {
  constructor() {
    super(null as unknown as DenericSchema);
  }
}

class NoStrictClass extends Deneric {
  number!: number
  string!: string
  boolean!: boolean
  object!: object
  array!: string[]

  constructor() {
    super({
      number: ['number', Number],
      string: ['string', String],
      boolean: ['boolean', Boolean],
      object: ['object', Object],
      array: ['array', Array]
    })
  }
}

class ArrayNumber extends Deneric {
  numbers!: number[]

  constructor(data?: object) {
    super({
      numbers: ['', Deneric.Array(Number)]
    })
    this.fromJson(data)
  }
}

const json1 = {
  profile: {
    full_name: 'John Smith',
    age: 12
  },
  others: {
    is_male: true,
    roles: ['1', '2', '2a', '2b']
  },
  jobs: {
    2021: ['A', 'B', 'C'],
    2025: ['B', 'D']
  },
  highscores: [10, 9.5, 8.75]
}

const json2 = null


const json3 = {
  profile: {
    full_name: 1,
    age: '12a'
  },
  others: {
    is_male: 5,
    roles: 'ABCDE'
  },
  jobs: true
}

const json4 = {
  my_student: [json1, json2, json3],
  class_monitor: json1,
  map_student: {
    json1,
    json2,
    json3
  }
}

const noStrictJson = {
  number: '123',
  string: 21,
  boolean: 'Not a Boolean',
  object: true,
  array: 'Not an Array'
}

describe('fromJson', () => {
  it('checking schema instance', () => {
    const r1 = new Student(json1)
    const r2 = new Student(json1)
    expect(Deneric.Utils.getSchema(r1)).to.be.equals(Deneric.Utils.getSchema(r2))
    expect(Deneric.Utils.getSchema(r1)).to.be.equals(STUDENT_SCHEMA)
  })
  it('checking parse data from schema', () => {
    const r = new Student(json1)
    expect(r.full_name).to.be.eq(json1.profile.full_name)
    expect(r.age).to.be.eq(json1.profile.age)
    expect(r.is_male).to.be.eq(json1.others.is_male)
    expect(r.roles).to.be.deep.equal(json1.others.roles)
    expect(r.jobs).to.be.deep.equal(json1.jobs)
    expect(r.highscores).to.be.deep.equal(json1.highscores)
  })
  it('checking parse default value (missing schema)', () => {
    const r = new Student(null)
    expect(r.full_name).to.be.eq('noname')
    expect(r.age).to.be.eq(-1)
    expect(r.is_male).to.be.eq(false)
    expect(r.roles).to.be.deep.equal(['100'])
    expect(r.jobs).to.be.deep.equal({ 2023: ['2023'] })
    expect(r.highscores).to.be.deep.equal([])

    r.highscores = [1]

    const r2 = r.clone<Student>()
    expect(r2.highscores).to.be.deep.equal(r.highscores)
  })
  it('checking parse default value (wrong data type)', () => {
    const r = new Student(json3)
    expect(r.full_name).to.be.eq('noname')
    expect(r.age).to.be.eq(-1)
    expect(r.is_male).to.be.eq(false)
    expect(r.roles).to.be.deep.equal(['100'])
    expect(r.jobs).to.be.deep.equal({ 2023: ['2023'] })
    expect(r.highscores).to.be.deep.equal([])
  })
  it('checking default value when call fromJson', () => {
    const r = new Student(json1)
    expect(r.full_name).to.be.eq(json1.profile.full_name)
    expect(r.age).to.be.eq(json1.profile.age)
    expect(r.is_male).to.be.eq(json1.others.is_male)
    expect(r.roles).to.be.deep.equal(json1.others.roles)
    expect(r.jobs).to.be.deep.equal(json1.jobs)

    r.fromJson(json3)
    expect(r.full_name).to.be.eq('noname')
    expect(r.age).to.be.eq(-1)
    expect(r.is_male).to.be.eq(false)
    expect(r.roles).to.be.deep.equal(['100'])
    expect(r.jobs).to.be.deep.equal({ 2023: ['2023'] })
  })
  it('checking parse complex data type (Array, Map)', () => {
    const r = new ClassRoom()
    const r2 = r.fromJson<ClassRoom>(json4)
    expect(r2.students.length).to.be.eq(json4.my_student.length)

    expect(r.students[0].full_name).to.be.eq(json1.profile.full_name)
    expect(r.students[0].age).to.be.eq(json1.profile.age)
    expect(r.students[0].is_male).to.be.eq(json1.others.is_male)
    expect(r.students[0].roles).to.be.deep.equal(json1.others.roles)
    expect(r.students[0].jobs).to.be.deep.equal(json1.jobs)

    expect(r.students[1].full_name).to.be.eq('noname')
    expect(r.students[1].age).to.be.eq(-1)
    expect(r.students[1].is_male).to.be.eq(false)
    expect(r.students[1].roles).to.be.deep.equal(['100'])
    expect(r.students[1].jobs).to.be.deep.equal({ 2023: ['2023'] })

    expect(r.students[2].full_name).to.be.eq('noname')
    expect(r.students[2].age).to.be.eq(-1)
    expect(r.students[2].is_male).to.be.eq(false)
    expect(r.students[2].roles).to.be.deep.equal(['100'])
    expect(r.students[2].jobs).to.be.deep.equal({ 2023: ['2023'] })

    expect(r.monitor.full_name).to.be.eq(json1.profile.full_name)
    expect(r.monitor.age).to.be.eq(json1.profile.age)
    expect(r.monitor.is_male).to.be.eq(json1.others.is_male)
    expect(r.monitor.roles).to.be.deep.equal(json1.others.roles)
    expect(r.monitor.jobs).to.be.deep.equal(json1.jobs)

    expect(r.mapStudents['json1'].full_name).to.be.eq(json1.profile.full_name)
    expect(r.mapStudents['json1'].age).to.be.eq(json1.profile.age)
    expect(r.mapStudents['json1'].is_male).to.be.eq(json1.others.is_male)
    expect(r.mapStudents['json1'].roles).to.be.deep.equal(json1.others.roles)
    expect(r.mapStudents['json1'].jobs).to.be.deep.equal(json1.jobs)

    expect(r.mapStudents['json2'].full_name).to.be.eq('noname')
    expect(r.mapStudents['json2'].age).to.be.eq(-1)
    expect(r.mapStudents['json2'].is_male).to.be.eq(false)
    expect(r.mapStudents['json2'].roles).to.be.deep.equal(['100'])
    expect(r.mapStudents['json2'].jobs).to.be.deep.equal({ 2023: ['2023'] })

    expect(r.mapStudents['json3'].full_name).to.be.eq('noname')
    expect(r.mapStudents['json3'].age).to.be.eq(-1)
    expect(r.mapStudents['json3'].is_male).to.be.eq(false)
    expect(r.mapStudents['json3'].roles).to.be.deep.equal(['100'])
    expect(r.mapStudents['json3'].jobs).to.be.deep.equal({ 2023: ['2023'] })
  })
})

describe('toJson test', () => {
  it('toJson default', () => {
    const r = new Student(json1)
    const json = r.toJson() as typeof json1

    expect(json.profile.full_name).to.be.eq(json1.profile.full_name)
    expect(json.profile.age).to.be.eq(json1.profile.age)
    expect(json.others.is_male).to.be.eq(json1.others.is_male)
    expect(json.others.roles).to.be.deep.equal(json1.others.roles)
    expect(json.jobs).to.be.deep.equal(json1.jobs)
  })
  it('toJson missing schema', () => {
    const r = new Student(json2)
    const json = r.toJson() as typeof json1
    expect(json.profile.full_name).to.be.eq('noname')
    expect(json.profile.age).to.be.eq(-1)
    expect(json.others.is_male).to.be.eq(false)
    expect(json.others.roles).to.be.deep.equal(['100'])
    expect(json.jobs).to.be.deep.equal({ 2023: ['2023'] })
  })
  it('toJson wrong data type', () => {
    const r = new Student(json3)
    const json = r.toJson() as typeof json1
    expect(json.profile.full_name).to.be.eq('noname')
    expect(json.profile.age).to.be.eq(-1)
    expect(json.others.is_male).to.be.eq(false)
    expect(json.others.roles).to.be.deep.equal(['100'])
    expect(json.jobs).to.be.deep.equal({ 2023: ['2023'] })
  })
  it('toJson with valid default value', () => {
    const n = new NoStrictClass()
    n.fromJson(noStrictJson, false)

    expect(typeof n.number === 'number')
    expect(typeof n.string === 'string')
    expect(typeof n.boolean === 'boolean')
    expect(Array.isArray(n.array))
    expect(typeof n.object === 'object')

    let r: { number?: number, string?: string, boolean?: boolean, array?: string[], object?: object } = n.toJson()
    expect(typeof r.number === 'number')
    expect(typeof r.string === 'string')
    expect(typeof r.boolean === 'boolean')
    expect(Array.isArray(r.array))
    expect(typeof r.object === 'object')

    // @ts-ignore
    n.number = undefined
    // @ts-ignore
    n.string = undefined
    // @ts-ignore
    n.boolean = undefined
    // @ts-ignore
    n.array = undefined
    // @ts-ignore
    n.object = undefined

    r = n.toJson()
    expect(typeof r.number === 'number')
    expect(typeof r.string === 'string')
    expect(typeof r.boolean === 'boolean')
    expect(Array.isArray(r.array))
    expect(typeof r.object === 'object')
  })
  it('toJson complex data type (Array, Map)', () => {
    const r = new ClassRoom()
    r.fromJson<ClassRoom>(json4)
    const json = r.toJson() as typeof json4

    expect(json.my_student.length).to.be.eq(json4.my_student.length)
    expect((json.my_student[0] as typeof json1).profile.full_name).to.be.eq(json1.profile.full_name)
    expect((json.my_student[0] as typeof json1).profile.age).to.be.eq(json1.profile.age)
    expect((json.my_student[0] as typeof json1).others.is_male).to.be.eq(json1.others.is_male)
    expect((json.my_student[0] as typeof json1).others.roles).to.be.deep.equal(json1.others.roles)
    expect((json.my_student[0] as typeof json1).jobs).to.be.deep.equal(json1.jobs)

    expect((json.my_student[1] as typeof json1).profile.full_name).to.be.eq('noname')
    expect((json.my_student[1] as typeof json1).profile.age).to.be.eq(-1)
    expect((json.my_student[1] as typeof json1).others.is_male).to.be.eq(false)
    expect((json.my_student[1] as typeof json1).others.roles).to.be.deep.equal(['100'])
    expect((json.my_student[1] as typeof json1).jobs).to.be.deep.equal({ 2023: ['2023'] })

    expect((json.my_student[2] as typeof json1).profile.full_name).to.be.eq('noname')
    expect((json.my_student[2] as typeof json1).profile.age).to.be.eq(-1)
    expect((json.my_student[2] as typeof json1).others.is_male).to.be.eq(false)
    expect((json.my_student[2] as typeof json1).others.roles).to.be.deep.equal(['100'])
    expect((json.my_student[2] as typeof json1).jobs).to.be.deep.equal({ 2023: ['2023'] })

    expect(json.class_monitor.profile.full_name).to.be.eq(json1.profile.full_name)
    expect(json.class_monitor.profile.age).to.be.eq(json1.profile.age)
    expect(json.class_monitor.others.is_male).to.be.eq(json1.others.is_male)
    expect(json.class_monitor.others.roles).to.be.deep.equal(json1.others.roles)
    expect(json.class_monitor.jobs).to.be.deep.equal(json1.jobs)

    expect((json.map_student['json1'] as typeof json1).profile.full_name).to.be.eq(json1.profile.full_name)
    expect((json.map_student['json1'] as typeof json1).profile.age).to.be.eq(json1.profile.age)
    expect((json.map_student['json1'] as typeof json1).others.is_male).to.be.eq(json1.others.is_male)
    expect((json.map_student['json1'] as typeof json1).others.roles).to.be.deep.equal(json1.others.roles)
    expect((json.map_student['json1'] as typeof json1).jobs).to.be.deep.equal(json1.jobs)

    expect((json.map_student['json2'] as unknown as typeof json1).profile.full_name).to.be.eq('noname')
    expect((json.map_student['json2'] as unknown as typeof json1).profile.age).to.be.eq(-1)
    expect((json.map_student['json2'] as unknown as typeof json1).others.is_male).to.be.eq(false)
    expect((json.map_student['json2'] as unknown as typeof json1).others.roles).to.be.deep.equal(['100'])
    expect((json.map_student['json2'] as unknown as typeof json1).jobs).to.be.deep.equal({ 2023: ['2023'] })

    expect((json.map_student['json3'] as unknown as typeof json1).profile.full_name).to.be.eq('noname')
    expect((json.map_student['json3'] as unknown as typeof json1).profile.age).to.be.eq(-1)
    expect((json.map_student['json3'] as unknown as typeof json1).others.is_male).to.be.eq(false)
    expect((json.map_student['json3'] as unknown as typeof json1).others.roles).to.be.deep.equal(['100'])
    expect((json.map_student['json3'] as unknown as typeof json1).jobs).to.be.deep.equal({ 2023: ['2023'] })
  })
  it('toJson must equal clone.toJson', () => {
    const r = new Student(json1)
    const json = r.toJson() as typeof json1
    const jsonClone = r.clone().toJson() as typeof json1

    expect(json).to.be.deep.equal(jsonClone)

    const r2 = new ClassRoom()
    r2.fromJson<ClassRoom>(json4)
    const json2 = r2.toJson() as typeof json4
    const json2Clone = r2.clone().toJson() as typeof json4
    expect(json2).to.be.deep.equal(json2Clone)
  })
})

describe('jsonIgnore test', () => {
  it('must be ignore json when call toJson func', () => {
    const r = new StudentIgnoreJob(json1)
    const json = r.toJson() as typeof json1

    expect(r.full_name).to.be.eq(json1.profile.full_name)
    expect(r.age).to.be.eq(json1.profile.age)
    expect(r.is_male).to.be.eq(json1.others.is_male)
    expect(r.roles).to.be.deep.equal(json1.others.roles)
    expect(r.jobs).to.be.deep.equal(json1.jobs)

    expect(json.profile.full_name).to.be.eq(json1.profile.full_name)
    expect(json.profile.age).to.be.eq(json1.profile.age)
    expect(json.others.is_male).to.be.eq(json1.others.is_male)
    expect(json.others.roles).to.be.deep.equal(json1.others.roles)
    expect(json.jobs).to.be.deep.equal(undefined)
  })
})

describe('Throw Error when Invalid Schema', () => {
  it('must be throw an error when using invalid schema', () => {
    expect(() => {
      new InvalidStudent()
    }).to.be.throw(TypeError, 'Invalid schema: InvalidStudent')
  })
})

describe('Common dataType and strict mode', () => {
  it('must be parse data to match dataType with no-strict rule', () => {
    const n = new NoStrictClass()
    n.fromJson(noStrictJson, false)

    expect(typeof n.number === 'number')
    expect(typeof n.string === 'string')
    expect(typeof n.boolean === 'boolean')
    expect(Array.isArray(n.array))
    expect(typeof n.object === 'object')

    expect(n.number).be.eq(Number(noStrictJson.number))
    expect(n.string).be.eq(String(noStrictJson.string))
    expect(n.boolean).be.eq(Boolean(noStrictJson.boolean))
    expect(n.array).be.deep.eq(Array(noStrictJson.array))
    expect(n.object).be.deep.eq(Object(noStrictJson.object))
  })

  it('must be parse to default value with strict rule', () => {
    const n = new NoStrictClass()
    n.fromJson(noStrictJson)

    expect(typeof n.number === 'number')
    expect(typeof n.string === 'string')
    expect(typeof n.boolean === 'boolean')
    expect(Array.isArray(n.array))
    expect(typeof n.object === 'object')

    expect(n.number).be.eq(0)
    expect(n.string).be.eq('')
    expect(n.boolean).be.eq(false)
    expect(n.array).be.deep.eq([])
    expect(n.object).be.deep.eq({})
  })
})

describe('Empty DataPath', () => {
  it('ArrayNumber', () => {
    const r = new ArrayNumber();
    expect(r.numbers).to.be.deep.equal([])

    const json = [1,2,3]
    r.fromJson(json)
    expect(r.numbers).to.be.deep.equal(json)

    const json2 = [3,5,7]
    const r2 = new ArrayNumber(json2);
    expect(r2.numbers).to.be.deep.equal(json2)
  })
})

const compositeJson = {
  name: 'parent',
  children: [{
    name: 'child_1'
  },{
    name: 'child_2'
  },{
    name: 'child_3',
    children: [{
      name: 'child_3_1',
    },{
      name: 'child_3_2'
    }]
  }]
}

class CompositeItem extends Deneric {
  name: string = ''
  children!: CompositeItem[]

  constructor(json?: object) {
    super({
      name: ['name', String],
      children: ['children', Deneric.Array(CompositeItem)]
    })
    this.fromJson(json)
  }
}

describe('Composite', () => {
  it('must be success when parse composite item', () => {
    const r = new CompositeItem(compositeJson)

    console.log('JSON', compositeJson)
    console.log('ENTITY', r)
    console.log('TO_JSON', r.toJson())
    console.log('TO_JSON_stringify', JSON.stringify(r.toJson()))
  })
})