import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server'
import { cleanDatabase } from '../../../db/factories'
import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../../db/neo4j'

const driver = getDriver()
const neode = getNeode()

let currentUser
let mutedUser
let authenticatedUser
let server

beforeAll(async () => {
  await cleanDatabase()
})

afterAll(async () => {
  await cleanDatabase()
  driver.close()
})

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

// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
afterEach(async () => {
  await cleanDatabase()
})

describe('mutedUsers', () => {
  let mutedUserQuery
  beforeEach(() => {
    mutedUserQuery = gql`
      query {
        mutedUsers {
          id
          name
          isMuted
        }
      }
    `
  })

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

  describe('authenticated and given a muted user', () => {
    beforeEach(async () => {
      currentUser = await neode.create('User', {
        name: 'Current User',
        id: 'u1',
      })
      mutedUser = await neode.create('User', {
        name: 'Muted User',
        id: 'u2',
      })
      await currentUser.relateTo(mutedUser, 'muted')
      authenticatedUser = await currentUser.toJson()
    })

    it('returns a list of muted users', async () => {
      const { query } = createTestClient(server)
      await expect(query({ query: mutedUserQuery })).resolves.toEqual(
        expect.objectContaining({
          data: {
            mutedUsers: [
              {
                name: 'Muted User',
                id: 'u2',
                isMuted: true,
              },
            ],
          },
        }),
      )
    })
  })
})

describe('muteUser', () => {
  let muteAction

  beforeEach(() => {
    currentUser = undefined
    muteAction = (variables) => {
      const { mutate } = createTestClient(server)
      const muteUserMutation = gql`
        mutation ($id: ID!) {
          muteUser(id: $id) {
            id
            name
            isMuted
          }
        }
      `
      return mutate({ mutation: muteUserMutation, variables })
    }
  })

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

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

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

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

    describe('given a to-be-muted user', () => {
      beforeEach(async () => {
        mutedUser = await neode.create('User', {
          name: 'Muted User',
          id: 'u2',
        })
      })

      it('mutes a user', async () => {
        await expect(muteAction({ id: 'u2' })).resolves.toEqual(
          expect.objectContaining({
            data: {
              muteUser: { id: 'u2', name: 'Muted User', isMuted: true },
            },
          }),
        )
      })

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

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

        beforeEach(async () => {
          const post1 = await neode.create('Post', {
            id: 'p12',
            title: 'A post written by the current user',
          })
          const post2 = await neode.create('Post', {
            id: 'p23',
            title: 'A post written by the muted user',
          })
          await Promise.all([
            post1.relateTo(currentUser, 'author'),
            post2.relateTo(mutedUser, '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 muted user',
                    author: {
                      name: 'Muted 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 mutes the other user', () => {
            beforeEach(async () => {
              await currentUser.relateTo(mutedUser, 'muted')
            })

            it("the muted 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 muted user', () => {
          beforeEach(async () => {
            authenticatedUser = await mutedUser.toJson()
          })

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

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

describe('unmuteUser', () => {
  let unmuteAction

  beforeEach(() => {
    currentUser = undefined
    unmuteAction = (variables) => {
      const { mutate } = createTestClient(server)
      const unmuteUserMutation = gql`
        mutation ($id: ID!) {
          unmuteUser(id: $id) {
            id
            name
            isMuted
          }
        }
      `
      return mutate({ mutation: unmuteUserMutation, variables })
    }
  })

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

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

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

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

    describe('given another user', () => {
      beforeEach(async () => {
        mutedUser = await neode.create('User', {
          name: 'Muted User',
          id: 'u2',
        })
      })

      describe('unmuting a not yet muted user', () => {
        it('does not hurt', async () => {
          await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
            expect.objectContaining({
              data: {
                unmuteUser: { id: 'u2', name: 'Muted User', isMuted: false },
              },
            }),
          )
        })
      })

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

        it('unmutes a user', async () => {
          await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
            expect.objectContaining({
              data: {
                unmuteUser: { id: 'u2', name: 'Muted User', isMuted: false },
              },
            }),
          )
        })

        describe('unmuting twice', () => {
          it('has no effect', async () => {
            await unmuteAction({ id: 'u2' })
            await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
              expect.objectContaining({
                data: {
                  unmuteUser: {
                    id: 'u2',
                    name: 'Muted User',
                    isMuted: false,
                  },
                },
              }),
            )
          })
        })
      })
    })
  })
})
