import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server'
import Factory from '../../../seed/factories'
import { gql } from '../../../jest/helpers'
import { neode, getDriver } from '../../../bootstrap/neo4j'

const driver = getDriver()
const factory = Factory()
const instance = neode()

let currentUser
let blockedUser
let authenticatedUser
let server

beforeEach(() => {
  authenticatedUser = undefined
  ;({ server } = createServer({
    context: () => {
      return {
        user: authenticatedUser,
        driver,
        neode: instance,
        cypherParams: {
          currentUserId: authenticatedUser ? authenticatedUser.id : null,
        },
      }
    },
  }))
})

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

describe('blockedUsers', () => {
  let blockedUserQuery
  beforeEach(() => {
    blockedUserQuery = gql`
      query {
        blockedUsers {
          id
          name
          isBlocked
        }
      }
    `
  })

  it('throws permission error', async () => {
    const { query } = createTestClient(server)
    const result = await query({ query: blockedUserQuery })
    expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
  })

  describe('authenticated and given a blocked user', () => {
    beforeEach(async () => {
      currentUser = await instance.create('User', {
        name: 'Current User',
        id: 'u1',
      })
      blockedUser = await instance.create('User', {
        name: 'Blocked User',
        id: 'u2',
      })
      await currentUser.relateTo(blockedUser, 'blocked')
      authenticatedUser = await currentUser.toJson()
    })

    it('returns a list of blocked users', async () => {
      const { query } = createTestClient(server)
      await expect(query({ query: blockedUserQuery })).resolves.toEqual(
        expect.objectContaining({
          data: {
            blockedUsers: [
              {
                name: 'Blocked User',
                id: 'u2',
                isBlocked: true,
              },
            ],
          },
        }),
      )
    })
  })
})

describe('block', () => {
  let blockAction

  beforeEach(() => {
    currentUser = undefined
    blockAction = variables => {
      const { mutate } = createTestClient(server)
      const blockMutation = gql`
        mutation($id: ID!) {
          block(id: $id) {
            id
            name
            isBlocked
          }
        }
      `
      return mutate({ mutation: blockMutation, variables })
    }
  })

  it('throws permission error', async () => {
    const result = await blockAction({ id: 'u2' })
    expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
  })

  describe('authenticated', () => {
    beforeEach(async () => {
      currentUser = await instance.create('User', {
        name: 'Current User',
        id: 'u1',
      })
      authenticatedUser = await currentUser.toJson()
    })

    describe('block yourself', () => {
      it('returns null', async () => {
        await expect(blockAction({ id: 'u1' })).resolves.toEqual(
          expect.objectContaining({ data: { block: null } }),
        )
      })
    })

    describe('block not existing user', () => {
      it('returns null', async () => {
        await expect(blockAction({ id: 'u2' })).resolves.toEqual(
          expect.objectContaining({ data: { block: null } }),
        )
      })
    })

    describe('given a to-be-blocked user', () => {
      beforeEach(async () => {
        blockedUser = await instance.create('User', {
          name: 'Blocked User',
          id: 'u2',
        })
      })

      it('blocks a user', async () => {
        await expect(blockAction({ id: 'u2' })).resolves.toEqual(
          expect.objectContaining({
            data: { block: { id: 'u2', name: 'Blocked User', isBlocked: true } },
          }),
        )
      })

      it('unfollows the user', async () => {
        await currentUser.relateTo(blockedUser, 'following')
        const queryUser = gql`
          query {
            User(id: "u2") {
              id
              isBlocked
              followedByCurrentUser
            }
          }
        `
        const { query } = createTestClient(server)
        await expect(query({ query: queryUser })).resolves.toEqual(
          expect.objectContaining({
            data: { User: [{ id: 'u2', isBlocked: false, followedByCurrentUser: true }] },
          }),
        )
        await blockAction({ id: 'u2' })
        await expect(query({ query: queryUser })).resolves.toEqual(
          expect.objectContaining({
            data: { User: [{ id: 'u2', isBlocked: true, followedByCurrentUser: false }] },
          }),
        )
      })

      describe('given both the current user and the to-be-blocked user write a post', () => {
        let postQuery

        beforeEach(async () => {
          const post1 = await instance.create('Post', {
            id: 'p12',
            title: 'A post written by the current user',
          })
          const post2 = await instance.create('Post', {
            id: 'p23',
            title: 'A post written by the blocked user',
          })
          await Promise.all([
            post1.relateTo(currentUser, 'author'),
            post2.relateTo(blockedUser, 'author'),
          ])
          postQuery = gql`
            query {
              Post(orderBy: createdAt_asc) {
                id
                title
                author {
                  id
                  name
                }
              }
            }
          `
        })

        const bothPostsAreInTheNewsfeed = async () => {
          const { query } = createTestClient(server)
          await expect(query({ query: postQuery })).resolves.toEqual(
            expect.objectContaining({
              data: {
                Post: [
                  {
                    id: 'p12',
                    title: 'A post written by the current user',
                    author: {
                      name: 'Current User',
                      id: 'u1',
                    },
                  },
                  {
                    id: 'p23',
                    title: 'A post written by the blocked user',
                    author: {
                      name: 'Blocked User',
                      id: 'u2',
                    },
                  },
                ],
              },
            }),
          )
        }

        describe('from the perspective of the current user', () => {
          it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)

          describe('but if the current user blocks the other user', () => {
            beforeEach(async () => {
              await currentUser.relateTo(blockedUser, 'blocked')
            })

            it("the blocked user's post won't show up in the newsfeed of the current user", async () => {
              const { query } = createTestClient(server)
              await expect(query({ query: postQuery })).resolves.toEqual(
                expect.objectContaining({
                  data: {
                    Post: [
                      {
                        id: 'p12',
                        title: 'A post written by the current user',
                        author: { name: 'Current User', id: 'u1' },
                      },
                    ],
                  },
                }),
              )
            })
          })
        })

        describe('from the perspective of the blocked user', () => {
          beforeEach(async () => {
            authenticatedUser = await blockedUser.toJson()
          })

          it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)
          describe('but if the current user blocks the other user', () => {
            beforeEach(async () => {
              await currentUser.relateTo(blockedUser, 'blocked')
            })

            it("the current user's post won't show up in the newsfeed of the blocked user", async () => {
              const { query } = createTestClient(server)
              await expect(query({ query: postQuery })).resolves.toEqual(
                expect.objectContaining({
                  data: {
                    Post: [
                      {
                        id: 'p23',
                        title: 'A post written by the blocked user',
                        author: { name: 'Blocked User', id: 'u2' },
                      },
                    ],
                  },
                }),
              )
            })
          })
        })
      })
    })
  })
})

describe('unblock', () => {
  let unblockAction

  beforeEach(() => {
    currentUser = undefined
    unblockAction = variables => {
      const { mutate } = createTestClient(server)
      const unblockMutation = gql`
        mutation($id: ID!) {
          unblock(id: $id) {
            id
            name
            isBlocked
          }
        }
      `
      return mutate({ mutation: unblockMutation, variables })
    }
  })

  it('throws permission error', async () => {
    const result = await unblockAction({ id: 'u2' })
    expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
  })

  describe('authenticated', () => {
    beforeEach(async () => {
      currentUser = await instance.create('User', {
        name: 'Current User',
        id: 'u1',
      })
      authenticatedUser = await currentUser.toJson()
    })

    describe('unblock yourself', () => {
      it('returns null', async () => {
        await expect(unblockAction({ id: 'u1' })).resolves.toEqual(
          expect.objectContaining({ data: { unblock: null } }),
        )
      })
    })

    describe('unblock not-existing user', () => {
      it('returns null', async () => {
        await expect(unblockAction({ id: 'lksjdflksfdj' })).resolves.toEqual(
          expect.objectContaining({ data: { unblock: null } }),
        )
      })
    })

    describe('given another user', () => {
      beforeEach(async () => {
        blockedUser = await instance.create('User', {
          name: 'Blocked User',
          id: 'u2',
        })
      })

      describe('unblocking a not yet blocked user', () => {
        it('does not hurt', async () => {
          await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
            expect.objectContaining({
              data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
            }),
          )
        })
      })

      describe('given a blocked user', () => {
        beforeEach(async () => {
          await currentUser.relateTo(blockedUser, 'blocked')
        })

        it('unblocks a user', async () => {
          await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
            expect.objectContaining({
              data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
            }),
          )
        })

        describe('unblocking twice', () => {
          it('has no effect', async () => {
            await unblockAction({ id: 'u2' })
            await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
              expect.objectContaining({
                data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
              }),
            )
          })
        })
      })
    })
  })
})
