import jsc from 'jsverify'
import update from 'immutability-helper'

import * as generators from './generators'
import * as utils from './utils'
import {
  actionsIncludingCrud,
  configureCrudReducer,
  crudReducer,
  defaultExtractors,
} from './crud'

const actions = actionsIncludingCrud('test')
const myCrudReducer = configureCrudReducer({
  error: action => action.payload.errors,
  index: action => action.payload.data,
  meta: action => action.payload.meta_data,
  single: action => action.payload,
})

describe('utils/crud', () => {
  describe('the default extractors', () => {
    ['index', 'error', 'single'].forEach(extractor => (
      describe(extractor, () => {
        test('returns the full action payload', () => {
          expect(defaultExtractors[extractor]({ payload: 'Hello' })).toEqual('Hello')
        })
      })
    ))

    describe('meta', () => {
      test('returns an empty object', () => {
        expect(defaultExtractors.meta({ payload: 'No please' })).toEqual({})
      })
    })
  })

  describe('the default crudReducer', () => {
    test('is a function', () => {
      expect(typeof crudReducer).toBe('function')
    })

    test('uses the full payload for all extractors except meta', () => {
      const expectedDefaultReducer = configureCrudReducer({
        index: action => action.payload,
        meta: () => ({}),
        error: action => action.payload,
        single: action => action.payload,
      })
      Object.values(actions)
        .filter(action => typeof action === 'function')
        .forEach(action => (
          jsc.assertForall(
            generators.crudReducerState,
            // Payload shape is irrelevant as long as we pass one
            generators.errorResponse,
            (state, payload) => {
              const before = JSON.stringify(state)
              const expected = JSON.stringify(expectedDefaultReducer, action(), actions)
              const result = JSON.stringify(myCrudReducer, action(payload), actions)
              const after = JSON.stringify(state)
              return before === after && result === expected
            },
          )
        ))
    })
  })

  describe('configureCrudReducer with example extractors', () => {
    test('is a function', () => {
      expect(typeof configureCrudReducer).toBe('function')
    })

    test('produces a function', () => {
      expect(typeof myCrudReducer).toBe('function')
    })

    describe('on empty action', () => {
      test('does not react', () => {
        jsc.assertForall(generators.crudReducerState, (state) => {
          const before = JSON.stringify(state)
          const result = JSON.stringify(myCrudReducer(state, {}, actions))
          const after = JSON.stringify(state)
          return before === result && result === after
        })
      })
    })

    describe('on CREATE', () => {
      test('just sets the create loading to true', () => {
        jsc.assertForall(generators.crudReducerState, jsc.dict(jsc.json), (state, payload) => {
          const before = JSON.stringify(state)
          const expected = JSON.stringify(update(state, { loading: { create: { $set: true } } }))
          const result = JSON.stringify(myCrudReducer(state, actions.create(payload), actions))
          const after = JSON.stringify(state)
          return before === after && result === expected
        })
      })
    })

    describe('on CREATE_SUCCESS', () => {
      test('sets the create loading to false, reset changes and sets single', () => {
        jsc.assertForall(generators.crudReducerState, jsc.dict(jsc.json), (state, payload) => {
          const before = JSON.stringify(state)
          const result = myCrudReducer(
            state,
            utils.action(actions.CREATE_SUCCESS, payload),
            actions,
          )
          const after = JSON.stringify(state)
          return before === after &&
            result.loading.create === false &&
            Object.keys(result.changes).length === 0 &&
            JSON.stringify(payload) === JSON.stringify(result.single)
        })
      })
    })

    describe('on CREATE_FAILURE', () => {
      test('sets the loading to false and the keeps the errors', () => {
        jsc.assertForall(
          generators.crudReducerState,
          generators.errorResponse,
          (state, payload) => {
            const before = JSON.stringify(state)
            const result = myCrudReducer(
              state,
              utils.action(actions.CREATE_FAILURE, payload),
              actions,
            )
            const after = JSON.stringify(state)
            return before === after &&
              result.loading.create === false &&
              result.errors.length === payload.errors.length
          },
        )
      })
    })

    describe('on DELETE', () => {
      test('sets the delete loading to true', () => {
        jsc.assertForall(
          generators.crudReducerState,
          generators.uuid,
          (state, payload) => {
            const before = JSON.stringify(state)
            const result = myCrudReducer(state, actions.delete(payload), actions)
            const after = JSON.stringify(state)
            return before === after && result.loading.delete === true
          },
        )
      })
    })

    describe('on DELETE_SUCCESS', () => {
      test('sets the delete loading to false', () => {
        jsc.assertForall(
          generators.crudReducerState,
          generators.uuid,
          (state, payload) => {
            const before = JSON.stringify(state)
            const result = myCrudReducer(
              state,
              utils.action(actions.DELETE_SUCCESS, payload),
              actions,
            )
            const after = JSON.stringify(state)
            return before === after && result.loading.delete === false
          },
        )
      })
    })

    describe('on DELETE_FAILURE', () => {
      test('sets the delete loading to false', () => {
        jsc.assertForall(
          generators.crudReducerState,
          generators.uuid,
          (state, payload) => {
            const before = JSON.stringify(state)
            const result = myCrudReducer(
              state,
              utils.action(actions.DELETE_FAILURE, payload),
              actions,
            )
            const after = JSON.stringify(state)
            return before === after && result.loading.delete === false
          },
        )
      })
    })

    describe('on FETCH_INDEX', () => {
      test('sets the index loading to true and ignores the payload', () => {
        jsc.assertForall(generators.crudReducerState, jsc.dict(jsc.json), (state, payload) => {
          const before = JSON.stringify(state)
          const expected = JSON.stringify(update(state, {
            loading: { index: { $set: true } },
            filters: { $set: payload },
          }))
          const result = JSON.stringify(myCrudReducer(state, actions.fetchIndex(payload), actions))
          const after = JSON.stringify(state)
          return before === after && result === expected
        })
      })
    })

    describe('on FETCH_INDEX_FAILURE', () => {
      test('sets the index loading to false', () => {
        jsc.assertForall(generators.crudReducerState, jsc.dict(jsc.json), (state, payload) => {
          const before = JSON.stringify(state)
          const expected = JSON.stringify(update(state, {
            loading: { index: { $set: false } },
          }))
          const result = JSON.stringify(myCrudReducer(
            state,
            utils.action(actions.FETCH_INDEX_FAILURE, payload),
            actions,
          ))
          const after = JSON.stringify(state)
          return before === after && result === expected
        })
      })
    })

    describe('on FETCH_INDEX_SUCCESS', () => {
      const indexResult = jsc.record({
        data: jsc.array(jsc.dict(jsc.json)),
        meta_data: jsc.record({ page: jsc.nat }),
      })

      test('sets loading to false, the index to the payload, and metadata accordingly', () => {
        jsc.assertForall(generators.crudReducerState, indexResult, (state, payload) => {
          const before = JSON.stringify(state)
          const result = myCrudReducer(
            state,
            utils.action(actions.FETCH_INDEX_SUCCESS, payload),
            actions,
          )
          const after = JSON.stringify(state)
          return before === after &&
            JSON.stringify(result.index) === JSON.stringify(payload.data) &&
            result.meta.page === payload.meta_data.page &&
            result.loading.index === false
        })
      })
    })

    describe('on FETCH_SINGLE', () => {
      test('sets loading to true and keeps single', () => {
        jsc.assertForall(generators.crudReducerState, generators.uuid, (state, payload) => {
          const before = JSON.stringify(state)
          const result = myCrudReducer(state, actions.fetchSingle(payload), actions)
          const after = JSON.stringify(state)
          return before === after &&
            JSON.stringify(state.single) === JSON.stringify(result.single) &&
            result.loading.single === true
        })
      })
    })

    describe('on FETCH_SINGLE_FAILURE', () => {
      test('sets loading to false', () => {
        jsc.assertForall(generators.crudReducerState, (state) => {
          const before = JSON.stringify(state)
          const result = myCrudReducer(state, utils.action(actions.FETCH_SINGLE_FAILURE, 'fail'), actions)
          const after = JSON.stringify(state)
          return before === after &&
            result.loading.single === false
        })
      })
    })

    describe('on FETCH_SINGLE_SUCCESS', () => {
      test('stores the fetched item', () => {
        jsc.assertForall(generators.crudReducerState, (state) => {
          const before = JSON.stringify(state)
          const result = myCrudReducer(state, utils.action(actions.FETCH_SINGLE_SUCCESS, 'thatsit'), actions)
          const after = JSON.stringify(state)
          return before === after &&
            result.loading.single === false &&
            result.single === 'thatsit'
        })
      })
    })

    describe('on UPDATE', () => {
      test('sets the loading state', () => {
        jsc.assertForall(generators.crudReducerState, (state) => {
          const before = JSON.stringify(state)
          const result = myCrudReducer(state, actions.update({ something: 'a' }), actions)
          const after = JSON.stringify(state)
          return before === after &&
            result.loading.update === true
        })
      })
    })

    describe('on UPDATE_SUCCESS', () => {
      test('resets the changes and saves the updated value', () => {
        jsc.assertForall(generators.crudReducerState, (state) => {
          const before = JSON.stringify(state)
          const result = myCrudReducer(state, utils.action(actions.UPDATE_SUCCESS, 'thatsit'), actions)
          const after = JSON.stringify(state)
          return before === after &&
            result.loading.update === false &&
            result.errors.length === 0 &&
            Object.keys(result.changes).length === 0 &&
            result.single === 'thatsit'
        })
      })

      describe('when the item is also present in index', () => {
        test('updates it', () => {
          jsc.assertForall(generators.crudReducerState, (state) => {
            if (state.index.length === 0) {
              return true
            }
            const randomIndex = Math.floor(Math.random() * state.index.length)
            const newIndex = [...state.index]
            newIndex[randomIndex] = { id: 'abc', name: 'thatsit' }
            const newState = {
              ...state,
              index: newIndex,
            }
            const result = myCrudReducer(newState, utils.action(actions.UPDATE_SUCCESS, {
              id: 'abc',
              name: 'thatsthat',
            }), actions)
            return result.index[randomIndex].id === 'abc' &&
              result.index[randomIndex].name === 'thatsthat' &&
              result.index.length === state.index.length
          })
        })
      })

      describe('when the item is not in index', () => {
        test('does not affect index', () => {
          jsc.assertForall(generators.crudReducerState, (state) => {
            const before = JSON.stringify(state.index)
            const result = myCrudReducer(state, utils.action(actions.UPDATE_SUCCESS, {
              id: 'abc',
              name: 'thatsit',
            }), actions)
            const after = JSON.stringify(state.index)
            return before === after && after === JSON.stringify(result.index)
          })
        })
      })
    })

    describe('on UPDATE_FAILURE', () => {
      test('sets loading update to false and saves the errors', () => {
        jsc.assertForall(
          generators.crudReducerState,
          generators.errorResponse,
          (state, payload) => {
            const before = JSON.stringify(state)
            const result = myCrudReducer(
              state,
              utils.action(actions.UPDATE_FAILURE, payload),
              actions,
            )
            const after = JSON.stringify(state)
            return before === after &&
              result.loading.update === false &&
              result.errors.length === payload.errors.length
          },
        )
      })
    })

    describe('on MERGE_CHANGES', () => {
      test('merges the changes', () => {
        const before = { changes: { a: 'b' } }
        const result = myCrudReducer(before, actions.mergeChanges({ potato: 'salad' }), actions)
        expect(result).toEqual({ changes: { a: 'b', potato: 'salad' } })
      })
    })

    describe('on SET_CHANGES', () => {
      test('sets the changes', () => {
        const before = { changes: { a: 'b' } }
        const result = myCrudReducer(before, actions.setChanges({ potato: 'salad' }), actions)
        expect(result).toEqual({ changes: { potato: 'salad' } })
      })
    })
  })
})

