import { TestApp } from '@baserow/test/helpers/testApp'
import Table from '@baserow/modules/database/pages/table'
import {
  DEFAULT_VIEW_ID_COOKIE_NAME,
  readDefaultViewIdFromCookie,
  decodeDefaultViewIdPerTable,
  encodeDefaultViewIdPerTable,
} from '@baserow/modules/database/utils/view'

// Mock out debounce so we don't have to wait or simulate waiting for the various
// debounces in the search functionality.
jest.mock('lodash/debounce', () => jest.fn((fn) => fn))

describe('View Tests', () => {
  let testApp = null
  let mockServer = null
  let originalReplaceFunc = null

  beforeAll(() => {
    testApp = new TestApp()
    mockServer = testApp.mockServer

    // Mock the redirect function so we can test the component without having to
    // worry about the router.
    originalReplaceFunc = testApp._app.$router.replace
    testApp._app.$router.replace = ({ params }) => {
      return Table.asyncData({
        app: testApp._app,
        store: testApp._app.store,
        params,
      })
    }
  })

  afterEach(() => testApp.afterEach())

  afterAll(() => {
    testApp._app.$router.replace = originalReplaceFunc
  })

  test('Default view is being set correctly initially', async () => {
    const allCookies = testApp.store.$cookies
    const { application, table, views } =
      await givenATableInTheServerWithMultipleViews()

    const gridView = views[0]
    const galleryView = views[1]
    // The first view is the Grid view, the Default view is the Gallery view which
    // is going to be rendered initially:
    const tableComponent = await testApp.mount(Table, {
      asyncDataParams: {
        databaseId: application.id,
        tableId: table.id,
        viewId: galleryView.id,
      },
    })

    const tableId = gridView.table_id

    // Check if Vuex store is updated correctly (first view):
    expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
    // Check if cookie is updated correctly (default view):
    const defaultViewId = readDefaultViewIdFromCookie(allCookies, tableId)
    expect(defaultViewId).not.toBe(null)
    const defaultView = testApp.store.getters['view/get'](defaultViewId)
    expect(defaultView.table_id).toBe(tableId)
    expect(defaultView.id).toBe(galleryView.id)
    // Check if Vuex store is updated correctly (default view):
    expect(testApp.store.getters['view/defaultId']).toBe(galleryView.id)
    // Check if component is rendered:
    expect(tableComponent.find('div.gallery-view').exists()).toBe(true)
    expect(tableComponent.find('div.grid-view').exists()).toBe(false)
  })

  test('Default view is being set correctly after changing views', async () => {
    const { application, table, views } =
      await givenATableInTheServerWithMultipleViews()

    const gridView = views[0]
    const galleryView = views[1]

    // The first view is the Grid view, the Default view is the Gallery view which
    // is going to be rendered initially:
    const tableComponent = await testApp.mount(Table, {
      asyncDataParams: {
        databaseId: application.id,
        tableId: table.id,
        viewId: galleryView.id,
      },
    })

    const allCookies = testApp.store.$cookies
    const tableId = gridView.table_id

    // Check if Vuex store is updated correctly (first view):
    expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
    // Check if cookie is updated correctly (default view):
    const defaultViewId = readDefaultViewIdFromCookie(allCookies, tableId)
    expect(defaultViewId).not.toBe(null)
    const defaultView = testApp.store.getters['view/get'](defaultViewId)
    expect(defaultView.table_id).toBe(tableId)
    expect(defaultView.id).toBe(galleryView.id)
    // Check if Vuex store is updated correctly (default view):
    expect(testApp.store.getters['view/defaultId']).toBe(galleryView.id)
    // Check if component is rendered:
    expect(tableComponent.find('div.gallery-view').exists()).toBe(true)
    expect(tableComponent.find('div.grid-view').exists()).toBe(false)

    // Let's switch back (select) the Grid (first) view:
    testApp.store.dispatch('view/selectById', gridView.id)

    // Check if Vuex store is updated correctly (first view):
    expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
    // Check if cookie is updated correctly (default view):
    const updatedCookieValue = decodeDefaultViewIdPerTable(
      allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
    )
    expect(updatedCookieValue.length).toBe(1)
    const updatedDefaultViewId = readDefaultViewIdFromCookie(
      allCookies,
      tableId
    )
    const updatedDefaultView =
      testApp.store.getters['view/get'](updatedDefaultViewId)
    expect(updatedDefaultView.table_id).toBe(tableId)
    expect(updatedDefaultView.id).toBe(gridView.id)
    // Check if Vuex store is updated correctly (default view):
    expect(testApp.store.getters['view/defaultId']).toBe(gridView.id)
  })

  test('Default view is being set correctly after switching tables', async () => {
    const { application, tables, views } =
      await givenATableInTheServerWithMultipleTables()

    const firstTable = tables[0]
    const secondTable = tables[1]
    const firstTableGridView = views[0]
    const secondTableGridView = views[1]

    // The first (and default) view is the Grid view, which is going to be rendered
    // initially for the firstTable:
    const firstTableComponent = await testApp.mount(Table, {
      asyncDataParams: {
        databaseId: application.id,
        tableId: firstTable.id,
      },
    })

    const allCookies = testApp.store.$cookies

    // Check if Vuex store is updated correctly (first view):
    expect(testApp.store.getters['view/first'].id).toBe(firstTableGridView.id)
    // Check if cookie is updated correctly (default view):
    const cookieValue = decodeDefaultViewIdPerTable(
      allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
    )
    expect(cookieValue.length).toBe(1)
    const defaultViewId = readDefaultViewIdFromCookie(
      allCookies,
      firstTableGridView.table_id
    )
    expect(defaultViewId).not.toBe(null)
    const defaultView = testApp.store.getters['view/get'](defaultViewId)
    expect(defaultView.table_id).toBe(firstTableGridView.table_id)
    expect(defaultView.id).toBe(firstTableGridView.id)
    // Check if Vuex store is updated correctly (default view):
    expect(testApp.store.getters['view/defaultId']).toBe(firstTableGridView.id)
    // Check if component is rendered:
    expect(firstTableComponent.find('div.grid-view').exists()).toBe(true)
    expect(firstTableComponent.find('div.gallery-view').exists()).toBe(false)

    // The first (and default) view is the Grid view, which is going to be rendered
    // initially for the secondTable:
    await testApp.mount(Table, {
      asyncDataParams: {
        databaseId: application.id,
        tableId: secondTable.id,
      },
    })

    // Let's switch to a different table in the database:
    testApp.store.dispatch('table/selectById', {
      databaseId: application.id,
      tableId: secondTable.id,
    })
    testApp.store.dispatch('view/selectById', secondTableGridView.id)

    const allCookiesAfterChangingTable = testApp.store.$cookies
    const cookieValueAfterChangingTable = decodeDefaultViewIdPerTable(
      allCookiesAfterChangingTable.get(DEFAULT_VIEW_ID_COOKIE_NAME)
    )

    // Check if Vuex store is updated correctly (first view):
    expect(testApp.store.getters['view/first'].id).toBe(secondTableGridView.id)
    // Check if cookie is updated correctly (default view):
    expect(cookieValueAfterChangingTable.length).toBe(2)
    const defaultViewIdAfterChangingTable = readDefaultViewIdFromCookie(
      allCookies,
      secondTableGridView.table_id
    )
    expect(defaultViewIdAfterChangingTable).not.toBe(null)
    const defaultViewAfterChangingTable = testApp.store.getters['view/get'](
      defaultViewIdAfterChangingTable
    )
    expect(defaultViewAfterChangingTable.table_id).toBe(
      secondTableGridView.table_id
    )
    expect(defaultViewAfterChangingTable.id).toBe(secondTableGridView.id)
    // Check if Vuex store is updated correctly (default view):
    expect(testApp.store.getters['view/defaultId']).toBe(secondTableGridView.id)
    // Check if component is rendered:
    expect(firstTableComponent.find('div.grid-view').exists()).toBe(true)
    expect(firstTableComponent.find('div.gallery-view').exists()).toBe(false)

    // Let's switch back to the first table in the database and see if first table's
    // default view is appended to the *end* of remembered views array:
    await testApp.mount(Table, {
      asyncDataParams: {
        databaseId: application.id,
        tableId: firstTable.id,
      },
    })
    testApp.store.dispatch('table/selectById', {
      databaseId: application.id,
      tableId: firstTable.id,
    })
    testApp.store.dispatch('view/selectById', firstTableGridView.id)

    const allCookiesAfterSwitchingBack = testApp.store.$cookies

    // Check if Vuex store is updated correctly (first view):
    expect(testApp.store.getters['view/first'].id).toBe(firstTableGridView.id)
    // Check if cookie is updated correctly (default view):
    const cookieValueAfterSwitchingBack = decodeDefaultViewIdPerTable(
      allCookiesAfterSwitchingBack.get(DEFAULT_VIEW_ID_COOKIE_NAME)
    )
    expect(cookieValueAfterSwitchingBack.length).toBe(2)
    const defaultViewIdAfterSwitchingBack = readDefaultViewIdFromCookie(
      allCookiesAfterSwitchingBack,
      firstTableGridView.table_id
    )
    expect(defaultViewIdAfterSwitchingBack).not.toBe(null)
    const defaultViewAfterSwitchingBack = testApp.store.getters['view/get'](
      defaultViewIdAfterSwitchingBack
    )
    expect(defaultViewAfterSwitchingBack.table_id).toBe(
      firstTableGridView.table_id
    )
    expect(defaultViewAfterSwitchingBack.id).toBe(firstTableGridView.id)
    // Check if Vuex store is updated correctly (default view):
    expect(testApp.store.getters['view/defaultId']).toBe(firstTableGridView.id)
  })

  test('Default view is being set correctly only from cookie', async () => {
    // set the cookie, render table without view id passed in, this should render
    // the default (Gallery) view
    const { application, table, views } =
      await givenATableInTheServerWithMultipleViews()

    const gridView = views[0]
    const galleryView = views[1]

    const tableId = gridView.table_id
    const allCookies = testApp.store.$cookies

    // Set the cookie for defaultView manually:
    const defaultViewIdData = []
    defaultViewIdData.push({
      tableId: galleryView.table_id,
      viewId: galleryView.id,
    })
    allCookies.set(
      DEFAULT_VIEW_ID_COOKIE_NAME,
      encodeDefaultViewIdPerTable(defaultViewIdData)
    )

    // The first view is the Grid view, the Default view is the Gallery view,
    // we're not rendering any view initially and Default view (Gallery view)
    // should be picked up from the cookie
    const tableComponent = await testApp.mount(Table, {
      asyncDataParams: {
        databaseId: application.id,
        tableId: table.id,
      },
    })

    // Check if Vuex store is updated correctly (first view):
    expect(testApp.store.getters['view/first'].id).toBe(gridView.id)
    // Check if cookie is updated correctly (default view):
    const cookieValue = decodeDefaultViewIdPerTable(
      allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
    )
    expect(cookieValue.length).toBe(1)
    const defaultViewId = readDefaultViewIdFromCookie(allCookies, tableId)
    expect(defaultViewId).not.toBe(null)
    const defaultView = testApp.store.getters['view/get'](defaultViewId)
    expect(defaultView.table_id).toBe(tableId)
    expect(defaultView.id).toBe(galleryView.id)
    // Check if Vuex store is updated correctly (default view):
    expect(testApp.store.getters['view/defaultId']).toBe(galleryView.id)
    // Check if component is rendered:
    expect(tableComponent.find('div.gallery-view').exists()).toBe(true)
    expect(tableComponent.find('div.grid-view').exists()).toBe(false)
  })

  test('Changing default view updates cookies array correctly', async () => {
    const { application, table, views } =
      await givenATableInTheServerWithMultipleViews()

    const gridView = views[0]

    // Generate random data to fill up the cookie
    // Our cookie has a limit of 2kb, so we need to generate enough data to fill it up
    // For sure one entry will need more than 1 byte, so we can't just generate 2048
    // entries
    const targetSize = 2048
    const randomData = []
    for (let i = 0; i < targetSize; i++) {
      const randomTableId = i
      const randomViewId = i
      const entry = { tableId: randomTableId, viewId: randomViewId }
      randomData.push(entry)
    }

    const allCookies = testApp.store.$cookies
    allCookies.set(
      DEFAULT_VIEW_ID_COOKIE_NAME,
      encodeDefaultViewIdPerTable(randomData)
    )
    const originalDataLength = randomData.length

    // Mount the component, which should update the cookies
    await testApp.mount(Table, {
      asyncDataParams: {
        databaseId: application.id,
        tableId: table.id,
        viewId: gridView.id,
      },
    })

    // The Default view is the Grid view and it should be set (appended) in the cookie
    const cookieValue = decodeDefaultViewIdPerTable(
      allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
    )
    expect(cookieValue.length).toBeGreaterThan(0)

    const defaultViewIdObject = cookieValue[cookieValue.length - 1]
    expect(defaultViewIdObject.tableId).toBe(gridView.table_id)
    expect(defaultViewIdObject.viewId).toBe(gridView.id)

    // Check if gridView is set as the last view in the array
    expect(cookieValue[cookieValue.length - 1]).toMatchObject(
      defaultViewIdObject
    )

    // Ensure that the first element is removed from the cookie array
    const updatedCookieValue = decodeDefaultViewIdPerTable(
      allCookies.get(DEFAULT_VIEW_ID_COOKIE_NAME)
    )
    expect(updatedCookieValue).not.toContainEqual(randomData[0])
    expect(updatedCookieValue.length).toBeLessThan(originalDataLength)
  })

  async function givenATableInTheServerWithMultipleViews() {
    const table = mockServer.createTable()
    const { application } = await mockServer.createAppAndWorkspace(table)
    const gridView = mockServer.createGridView(application, table, {
      viewId: 1,
    })
    const galleryView = mockServer.createGalleryView(application, table, {
      viewId: 2,
    })

    mockServer.mock
      .onGet(`/database/views/table/${table.id}/`)
      .reply(200, [gridView, galleryView])

    const fields = mockServer.createFields(application, table, [
      {
        name: 'Name',
        type: 'text',
        primary: true,
      },
      {
        name: 'Last name',
        type: 'text',
      },
      {
        name: 'Notes',
        type: 'long_text',
      },
      {
        name: 'Active',
        type: 'boolean',
      },
    ])

    const rows = [
      {
        id: 1,
        order: 0,
        field_1: 'name',
        field_2: 'last_name',
        field_3: 'notes',
        field_4: false,
      },
    ]

    mockServer.createGridRows(gridView, fields, rows)
    mockServer.createFields(application, table, fields)
    mockServer.createGalleryRows(galleryView, fields, rows)

    const views = []
    views.push(gridView)
    views.push(galleryView)
    return { application, table, views }
  }

  async function givenATableInTheServerWithMultipleTables() {
    const firstTable = mockServer.createTable(1, 'Test Table 1')
    const secondTable = mockServer.createTable(2, 'Test Table 2')
    const { application } =
      await mockServer.createAppAndWorkspaceWithMultipleTables([
        firstTable,
        secondTable,
      ])

    // First table - 1 view:
    const firstTableGridView = mockServer.createGridView(
      application,
      firstTable,
      {
        viewId: 1,
      }
    )
    mockServer.mock
      .onGet(`/database/views/table/${firstTable.id}/`)
      .reply(200, [firstTableGridView])

    // Second table - 1 view:
    const secondTableGridView = mockServer.createGridView(
      application,
      secondTable,
      {
        viewId: 2,
      }
    )
    mockServer.mock
      .onGet(`/database/views/table/${secondTable.id}/`)
      .reply(200, [secondTableGridView])

    const fields = mockServer.createFields(application, firstTable, [
      {
        name: 'Name',
        type: 'text',
        primary: true,
      },
      {
        name: 'Last name',
        type: 'text',
      },
      {
        name: 'Notes',
        type: 'long_text',
      },
      {
        name: 'Active',
        type: 'boolean',
      },
    ])

    const rows = [
      {
        id: 1,
        order: 0,
        field_1: 'name',
        field_2: 'last_name',
        field_3: 'notes',
        field_4: false,
      },
    ]

    mockServer.createGridRows(firstTableGridView, fields, rows)
    mockServer.createGridRows(secondTableGridView, fields, rows)
    mockServer.createFields(application, firstTable, fields)
    mockServer.createFields(application, secondTable, fields)

    const views = []
    views.push(firstTableGridView)
    views.push(secondTableGridView)

    const tables = []
    tables.push(firstTable)
    tables.push(secondTable)
    return { application, tables, views }
  }
})