import {
  add,
  compose,
  curry,
  toUpper,
} from 'ramda'
import { Nullable } from '.'

describe('the Nullable module', () => {
  const name: string = 'noob noob'

  describe('isNone', () => {
    it('determines if a particular Nullable is None', () => {
      expect(Nullable.isNone(null)).toBe(true)
      expect(Nullable.isNone('hi')).toBe(false)
    })

    it('provides a typeguard', () => {
      const handleNullable = (someNullable: Nullable<string>): string => {
        if (!Nullable.isNone(someNullable)) {
          return someNullable
        }
        return 'value was undefined'
      }
      expect(handleNullable('oooh wee!')).toBe('oooh wee!')
      expect(handleNullable(null)).toBe('value was undefined')
    })
  })

  describe('isSome', () => {
    it('determines if a particular Nullable is a concrete value', () => {
      expect(Nullable.isSome(null)).toBe(false)
      expect(Nullable.isSome('hi')).toBe(true)
    })

    it('provides a typeguard', () => {
      const handleNullable = (someNullable: Nullable<string>): string => {
        if (Nullable.isSome(someNullable)) {
          return someNullable
        }
        return 'value was undefined'
      }
      expect(handleNullable('oooh wee!')).toBe('oooh wee!')
      expect(handleNullable(null)).toBe('value was undefined')
    })
  })

  describe('Nullable.map', () => {
    describe('when given a None', () => {
      it('returns a None', () => {
        const result = Nullable.map(toUpper)(null)
        expect(result).toEqual(null)
      })
    })

    describe('when given a concrete value', () => {
      it('applies the provided function to the concrete value value', () => {
        const result = Nullable.map(toUpper, name)
        expect(result).toEqual(toUpper(name))
      })
    })
  })

  describe('Nullable.andThen', () => {
    const safeDivide = curry((a: number, b: number): Nullable<number> => {
      return a === 0
        ? null
        : b / a
    })

    describe('when we encounter a None in a composition chain', () => {
      it('gracefully handles the absence', () => {
        const result = compose(
          Nullable.andThen(safeDivide(3)),
          Nullable.andThen(safeDivide(0)),
          Nullable.andThen(safeDivide(4)),
          safeDivide(2),
        )(32)
        expect(result).toEqual(null)
      })
    })

    describe('when a composition chain works out according to plan', () => {
      it('returns our desired result', () => {
        const result = compose(
          Nullable.andThen(safeDivide(3)),
          Nullable.andThen(safeDivide(5)),
          Nullable.andThen(safeDivide(4)),
          safeDivide(2),
        )(32)
        expect(result).toEqual(32 / 2 / 4 / 5 / 3)
      })
    })
  })

  describe('Nullable.withDefault', () => {
    describe('when given a default value and a None', () => {
      it('returns the default value', () => {
        const result = Nullable.withDefault(name)(null)
        expect(result).toBe(name)
      })
    })

    describe('when given a default value and a concrete value', () => {
      it('returns the concrete value value', () => {
        const result = Nullable.withDefault<string>('foo', name)
        expect(result).toBe(name)
      })
    })
  })

  describe('Nullable.ap', () => {
    describe('when the applicative Nullable is None', () => {
      it('returns a None', () => {
        const result = Nullable.ap(name)(null)
        expect(result).toEqual(null)
      })
    })

    describe('when the target Nullable is None', () => {
      it('returns a None', () => {
        const result = Nullable.ap(null as Nullable<string>)(toUpper)
        expect(result).toEqual(null)
      })
    })

    describe('when both the applicative and target Nullables are Justs', () => {
      it('applies the wrapped function in the applicative Nullable to value in the target Nullable', () => {
        const result = Nullable.ap(name)(toUpper)
        expect(result).toEqual(toUpper(name))
      })
    })

    describe('lifting functions', () => {
      const addThreeNumbers = (a: number) => (b: number) => (c: number) => a + b + c

      describe('when everything goes according to plan', () => {
        it('lifts a "regular" function into Nullable context and returns the correct value', () => {
          const result = compose(
            Nullable.ap(3),
            Nullable.ap(2),
            Nullable.ap(1),
          )(addThreeNumbers)
          expect(result).toBe(1 + 2 + 3)
        })
      })

      describe('when we exprience an absence', () => {
        it('lifts a "regular" function into Nullable context and returns null', () => {
          const result = compose(
            Nullable.ap(3),
            Nullable.ap(null as Nullable<number>),
            Nullable.ap(1),
          )(addThreeNumbers)
          expect(result).toBe(null)
        })
      })
    })
  })

  describe('Nullable.maybe', () => {
    describe('when given a None', () => {
      it('returns the default value', () => {
        expect(Nullable.maybe(7, add(83), null)).toBe(7)
      })
    })

    describe('when given a concrete value', () => {
      it('applies the provided function to the concrete value and returns the resulting return value', () => {
        expect(Nullable.maybe(7, add(83), 34)).toBe(83 + 34)
      })
    })
  })
})
