import jwt from 'jsonwebtoken'
import CONFIG from './../../config'
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server'
import encode from '../../jwt/encode'

const factory = Factory()
let query
let mutate
let variables
let req
let user

const disable = async id => {
  await factory.create('User', { id: 'u2', role: 'moderator' })
  const moderatorBearerToken = encode({ id: 'u2' })
  req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } }
  await mutate({
    mutation: gql`
      mutation($id: ID!) {
        disable(id: $id)
      }
    `,
    variables: { id },
  })
  req = { headers: {} }
}

beforeEach(() => {
  user = null
  req = { headers: {} }
})

beforeAll(() => {
  const { server } = createServer({
    context: () => {
      // One of the rare occasions where we test
      // the actual `context` implementation here
      return context({ req })
    },
  })
  query = createTestClient(server).query
  mutate = createTestClient(server).mutate
})

afterEach(async () => {
  await factory.cleanDatabase()
})

describe('isLoggedIn', () => {
  const isLoggedInQuery = gql`
    {
      isLoggedIn
    }
  `
  const respondsWith = async expected => {
    await expect(query({ query: isLoggedInQuery })).resolves.toMatchObject(expected)
  }

  describe('unauthenticated', () => {
    it('returns false', async () => {
      await respondsWith({ data: { isLoggedIn: false } })
    })
  })

  describe('authenticated', () => {
    beforeEach(async () => {
      user = await factory.create('User', { id: 'u3' })
      const userBearerToken = encode({ id: 'u3' })
      req = { headers: { authorization: `Bearer ${userBearerToken}` } }
    })

    it('returns true', async () => {
      await respondsWith({ data: { isLoggedIn: true } })
    })

    describe('but user is disabled', () => {
      beforeEach(async () => {
        await disable('u3')
      })

      it('returns false', async () => {
        await respondsWith({ data: { isLoggedIn: false } })
      })
    })

    describe('but user is deleted', () => {
      beforeEach(async () => {
        await user.update({ updatedAt: new Date().toISOString(), deleted: true })
      })

      it('returns false', async () => {
        await respondsWith({ data: { isLoggedIn: false } })
      })
    })
  })
})

describe('currentUser', () => {
  const currentUserQuery = gql`
    {
      currentUser {
        id
        slug
        name
        avatar
        email
        role
      }
    }
  `

  const respondsWith = async expected => {
    await expect(query({ query: currentUserQuery, variables })).resolves.toMatchObject(expected)
  }

  describe('unauthenticated', () => {
    it('returns null', async () => {
      await respondsWith({ data: { currentUser: null } })
    })
  })

  describe('authenticated', () => {
    describe('and corresponding user in the database', () => {
      beforeEach(async () => {
        await factory.create('User', {
          id: 'u3',
          // the `id` is the only thing that has to match the decoded JWT bearer token
          avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
          email: 'test@example.org',
          name: 'Matilde Hermiston',
          slug: 'matilde-hermiston',
          role: 'user',
        })
        const userBearerToken = encode({ id: 'u3' })
        req = { headers: { authorization: `Bearer ${userBearerToken}` } }
      })

      it('returns the whole user object', async () => {
        const expected = {
          data: {
            currentUser: {
              id: 'u3',
              avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
              email: 'test@example.org',
              name: 'Matilde Hermiston',
              slug: 'matilde-hermiston',
              role: 'user',
            },
          },
        }
        await respondsWith(expected)
      })
    })
  })
})

describe('login', () => {
  const loginMutation = gql`
    mutation($email: String!, $password: String!) {
      login(email: $email, password: $password)
    }
  `

  const respondsWith = async expected => {
    await expect(mutate({ mutation: loginMutation, variables })).resolves.toMatchObject(expected)
  }

  beforeEach(async () => {
    variables = { email: 'test@example.org', password: '1234' }
    user = await factory.create('User', {
      ...variables,
      id: 'acb2d923-f3af-479e-9f00-61b12e864666',
    })
  })

  describe('ask for a `token`', () => {
    describe('with a valid email/password combination', () => {
      it('responds with a JWT bearer token', async done => {
        const {
          data: { login: token },
        } = await mutate({ mutation: loginMutation, variables })
        jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
          expect(data.email).toEqual('test@example.org')
          expect(err).toBeNull()
          done()
        })
      })

      describe('but user account is deleted', () => {
        beforeEach(async () => {
          await user.update({ updatedAt: new Date().toISOString(), deleted: true })
        })

        it('responds with "Incorrect email address or password."', async () => {
          await respondsWith({
            data: null,
            errors: [{ message: 'Incorrect email address or password.' }],
          })
        })
      })

      describe('but user account is disabled', () => {
        beforeEach(async () => {
          await disable('acb2d923-f3af-479e-9f00-61b12e864666')
        })

        it('responds with "Your account has been disabled."', async () => {
          await respondsWith({
            data: null,
            errors: [{ message: 'Your account has been disabled.' }],
          })
        })
      })
    })

    describe('with a valid email but incorrect password', () => {
      beforeEach(() => {
        variables = { ...variables, email: 'test@example.org', password: 'wrong' }
      })

      it('responds with "Incorrect email address or password."', async () => {
        await respondsWith({
          errors: [{ message: 'Incorrect email address or password.' }],
        })
      })
    })

    describe('with a non-existing email', () => {
      beforeEach(() => {
        variables = {
          ...variables,
          email: 'non-existent@example.org',
          password: '1234',
        }
      })

      it('responds with "Incorrect email address or password."', async () => {
        await respondsWith({
          errors: [{ message: 'Incorrect email address or password.' }],
        })
      })
    })
  })
})

describe('change password', () => {
  const changePasswordMutation = gql`
    mutation($oldPassword: String!, $newPassword: String!) {
      changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
    }
  `

  const respondsWith = async expected => {
    await expect(mutate({ mutation: changePasswordMutation, variables })).resolves.toMatchObject(
      expected,
    )
  }

  beforeEach(async () => {
    variables = { ...variables, oldPassword: 'what', newPassword: 'ever' }
  })

  describe('unauthenticated', () => {
    it('throws "Not Authorised!"', async () => {
      await respondsWith({ errors: [{ message: 'Not Authorised!' }] })
    })
  })

  describe('authenticated', () => {
    beforeEach(async () => {
      await factory.create('User', { id: 'u3' })
      const userBearerToken = encode({ id: 'u3' })
      req = { headers: { authorization: `Bearer ${userBearerToken}` } }
    })
    describe('old password === new password', () => {
      beforeEach(() => {
        variables = { ...variables, oldPassword: '1234', newPassword: '1234' }
      })

      it('responds with "Old password and new password should be different"', async () => {
        await respondsWith({
          errors: [{ message: 'Old password and new password should be different' }],
        })
      })
    })

    describe('incorrect old password', () => {
      beforeEach(() => {
        variables = {
          ...variables,
          oldPassword: 'notOldPassword',
          newPassword: '12345',
        }
      })

      it('responds with "Old password isn\'t valid"', async () => {
        await respondsWith({ errors: [{ message: 'Old password is not correct' }] })
      })
    })

    describe('correct password', () => {
      beforeEach(() => {
        variables = {
          ...variables,
          oldPassword: '1234',
          newPassword: '12345',
        }
      })

      it('changes the password if given correct credentials "', async () => {
        await respondsWith({ data: { changePassword: expect.any(String) } })
      })
    })
  })
})
