import localStorage from '../../../../frontend/js/infrastructure/local-storage'
import PdfPreview from '../../../../frontend/js/features/pdf-preview/components/pdf-preview'
import { EditorProviders } from '../../helpers/editor-providers'
import { mockScope } from './scope'

const storeAndFireEvent = (win: typeof window, key: string, value: unknown) => {
  localStorage.setItem(key, value)
  win.dispatchEvent(new StorageEvent('storage', { key }))
}

describe('<PdfPreview/>', function () {
  beforeEach(function () {
    cy.interceptCompile()
    cy.interceptEvents()
  })

  it('renders the PDF preview', function () {
    const scope = mockScope()

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    )

    // wait for "compile on load" to finish
    cy.findByRole('button', { name: 'Compiling…' })
    cy.wait('@compile')
    cy.findByRole('button', { name: 'Recompile' })
    cy.wait('@compile-pdf')
  })

  it('runs a compile when the Recompile button is pressed', function () {
    const scope = mockScope()

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    )

    // wait for "compile on load" to finish
    cy.findByRole('button', { name: 'Compiling…' })
    cy.wait('@compile')
    cy.wait('@compile-pdf')

    cy.interceptCompile('recompile')

    // press the Recompile button => compile
    cy.findByRole('button', { name: 'Recompile' }).click()

    // wait for "recompile" to finish
    // cy.findByRole('button', { name: 'Compiling…' })
    cy.wait('@recompile-pdf')
    cy.wait('@recompile-log')
    cy.wait('@recompile-blg')

    cy.findByRole('button', { name: 'Recompile' })

    cy.contains('Your Paper')
  })

  it('runs a compile on `pdf:recompile` event', function () {
    const scope = mockScope()

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    )

    // wait for "compile on load" to finish
    cy.findByRole('button', { name: 'Compiling…' })
    cy.wait('@compile')
    cy.wait('@compile-pdf')

    cy.interceptCompile('recompile')

    cy.window().then(win => {
      win.dispatchEvent(new CustomEvent('pdf:recompile'))
    })

    // wait for "recompile" to finish
    // cy.findByRole('button', { name: 'Compiling…' })
    cy.wait('@recompile')

    cy.findByRole('button', { name: 'Recompile' })

    cy.wait('@recompile-pdf')
    cy.contains('Your Paper')
  })

  it('does not compile while compiling', function () {
    let compileResolve: (value?: unknown) => void
    let counter = 0

    const promise = new Promise(resolve => {
      compileResolve = resolve
    })

    cy.intercept(
      'POST',
      '/project/project123/compile?auto_compile=true',
      req => {
        counter++

        promise.then(() => {
          req.reply({
            body: {
              status: 'success',
              clsiServerId: 'foo',
              compileGroup: 'priority',
              pdfDownloadDomain: 'https://clsi.test-overleaf.com',
              outputFiles: [
                {
                  path: 'output.pdf',
                  build: '123',
                  url: '/build/123/output.pdf',
                  type: 'pdf',
                },
                {
                  path: 'output.log',
                  build: '123',
                  url: '/build/123/output.log',
                  type: 'log',
                },
              ],
            },
          })
        })

        return promise
      }
    ).as('compile')

    const scope = mockScope()

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    ).then(() => {
      cy.findByRole('button', { name: 'Compiling…' })

      cy.window().then(win => {
        win.dispatchEvent(new CustomEvent('pdf:recompile'))
      })

      compileResolve()

      cy.findByRole('button', { name: 'Recompile' })

      cy.contains('Your Paper').should(() => {
        expect(counter).to.equal(1)
      })
    })
  })

  it('disables compile button while compile is running', function () {
    const scope = mockScope()

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    )

    cy.findByRole('button', { name: 'Compiling…' }).should('be.disabled')
    cy.findByRole('button', { name: 'Recompile' }).should('not.be.disabled')
  })

  it('runs a compile on doc change if autocompile is enabled', function () {
    const scope = mockScope()

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    )

    // wait for "compile on load" to finish
    cy.findByRole('button', { name: 'Compiling…' })
    cy.wait('@compile')
    cy.findByRole('button', { name: 'Recompile' })

    cy.window().then(win => {
      cy.clock()

      // switch on auto compile
      storeAndFireEvent(win, 'autocompile_enabled:project123', true)

      // fire a doc:changed event => compile
      win.dispatchEvent(new CustomEvent('doc:changed'))

      cy.tick(5000) // AUTO_COMPILE_DEBOUNCE

      cy.clock().invoke('restore')
    })

    cy.findByRole('button', { name: 'Compiling…' })
    cy.findByRole('button', { name: 'Recompile' })
  })

  it('does not run a compile on doc change if autocompile is disabled', function () {
    const scope = mockScope()

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    )

    // wait for "compile on load" to finish
    cy.findByRole('button', { name: 'Compiling…' })
    cy.findByRole('button', { name: 'Recompile' })

    cy.window().then(win => {
      cy.clock()

      // make sure auto compile is switched off
      storeAndFireEvent(win, 'autocompile_enabled:project123', false)

      // fire a doc:changed event => no compile
      win.dispatchEvent(new CustomEvent('doc:changed'))

      cy.tick(5000) // AUTO_COMPILE_DEBOUNCE

      cy.clock().invoke('restore')
    })

    cy.findByRole('button', { name: 'Recompile' })
  })

  it('does not run a compile on doc change if autocompile is blocked by syntax check', function () {
    const scope = mockScope()
    // enable linting in the editor
    scope.settings.syntaxValidation = true
    // mock a linting error
    scope.hasLintingError = true

    cy.mount(
      <EditorProviders scope={scope}>
        <div className="pdf-viewer">
          <PdfPreview />
        </div>
      </EditorProviders>
    )

    // wait for "compile on load" to finish
    cy.findByRole('button', { name: 'Compiling…' })
    cy.findByRole('button', { name: 'Recompile' })

    cy.window().then(win => {
      cy.clock()

      // switch on auto compile
      storeAndFireEvent(win, 'autocompile_enabled:project123', true)

      // switch on syntax checking
      storeAndFireEvent(win, 'stop_on_validation_error', true)

      // fire a doc:changed event => no compile
      win.dispatchEvent(new CustomEvent('doc:changed'))

      cy.tick(5000) // AUTO_COMPILE_DEBOUNCE

      cy.clock().invoke('restore')
    })

    cy.findByRole('button', { name: 'Recompile' })
    cy.findByText('Code check failed')
  })

  describe('displays error messages', function () {
    const compileErrorStatuses = {
      'clear-cache':
        'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
      'clsi-maintenance':
        'The compile servers are down for maintenance, and will be back shortly.',
      'compile-in-progress':
        'A previous compile is still running. Please wait a minute and try compiling again.',
      exited: 'Server Error',
      failure: 'No PDF',
      generic: 'Server Error',
      'project-too-large': 'Project too large',
      'rate-limited': 'Compile rate limit hit',
      terminated: 'Compilation cancelled',
      timedout: 'Timed out',
      'too-recently-compiled':
        'This project was compiled very recently, so this compile has been skipped.',
      unavailable:
        'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.',
      foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
    }

    for (const [status, message] of Object.entries(compileErrorStatuses)) {
      it(`displays error message for '${status}' status`, function () {
        cy.intercept('POST', '/project/*/compile?*', {
          body: {
            status,
            clsiServerId: 'foo',
            compileGroup: 'priority',
          },
        }).as('compile')

        const scope = mockScope()

        cy.mount(
          <EditorProviders scope={scope}>
            <div className="pdf-viewer">
              <PdfPreview />
            </div>
          </EditorProviders>
        )

        // wait for "compile on load" to finish
        cy.findByRole('button', { name: 'Compiling…' })
        cy.findByRole('button', { name: 'Recompile' })

        cy.findByText(message)
      })
    }

    it('displays expandable raw logs', function () {
      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      // wait for "compile on load" to finish
      cy.findByRole('button', { name: 'Compiling…' })
      cy.findByRole('button', { name: 'Recompile' })

      cy.findByRole('button', { name: 'View logs' }).click()
      cy.findByRole('button', { name: 'View PDF' })

      cy.findByRole('button', { name: 'Expand' }).click()
      cy.findByRole('button', { name: 'Collapse' }).click()
    })

    it('displays error messages if there were validation problems', function () {
      const validationProblems = {
        sizeCheck: {
          resources: [
            { path: 'foo/bar', kbSize: 76221 },
            { path: 'bar/baz', kbSize: 2342 },
          ],
        },
        mainFile: true,
        conflictedPaths: [
          {
            path: 'foo/bar',
          },
          {
            path: 'foo/baz',
          },
        ],
      }

      cy.intercept('POST', '/project/*/compile?*', {
        body: {
          status: 'validation-problems',
          validationProblems,
          clsiServerId: 'foo',
          compileGroup: 'priority',
        },
      }).as('compile')

      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      // wait for "compile on load" to finish
      cy.findByRole('button', { name: 'Compiling…' })
      cy.findByRole('button', { name: 'Recompile' })

      cy.wait('@compile')

      cy.findByText('Project too large')
      cy.findByText('Unknown main document')
      cy.findByText('Conflicting Paths Found')
    })

    it('sends a clear cache request when the button is pressed', function () {
      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      // wait for "compile on load" to finish
      cy.findByRole('button', { name: 'Compiling…' })
      cy.findByRole('button', { name: 'Recompile' })

      cy.findByRole('button', { name: 'View logs' }).click()
      cy.findByRole('button', { name: 'Clear cached files' }).should(
        'not.be.disabled'
      )

      cy.intercept('DELETE', 'project/*/output?*', {
        statusCode: 204,
        delay: 100,
      }).as('clear-cache')

      // click the button
      cy.findByRole('button', { name: 'Clear cached files' }).click()
      cy.findByRole('button', { name: 'Clear cached files' }).should(
        'be.disabled'
      )
      cy.wait('@clear-cache')
      cy.findByRole('button', { name: 'Clear cached files' }).should(
        'not.be.disabled'
      )
    })

    it('handle "recompile from scratch"', function () {
      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      // wait for "compile on load" to finish
      cy.findByRole('button', { name: 'Compiling…' })
      cy.findByRole('button', { name: 'Recompile' })

      // show the logs UI
      cy.findByRole('button', { name: 'View logs' }).click()

      cy.findByRole('button', { name: 'Clear cached files' }).should(
        'not.be.disabled'
      )

      cy.interceptCompile()

      cy.intercept('DELETE', 'project/*/output?*', {
        statusCode: 204,
        delay: 100,
      }).as('clear-cache')

      // TODO: open the menu?
      cy.findByRole('menuitem', {
        name: 'Recompile from scratch',
        hidden: true,
      }).trigger('click', { force: true })

      cy.findByRole('button', { name: 'Clear cached files' }).should(
        'be.disabled'
      )

      cy.findByRole('button', { name: 'Compiling…' })
      cy.wait('@clear-cache')
      cy.findByRole('button', { name: 'Recompile' })

      cy.wait('@compile')
      cy.wait('@compile-pdf')
    })

    it('shows an error for an invalid URL', function () {
      cy.intercept('/build/*/output.pdf?*', {
        statusCode: 500,
        body: {
          message: 'something awful happened',
          code: 'AWFUL_ERROR',
        },
      }).as('compile-pdf-error')

      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      cy.wait('@compile-pdf-error')

      cy.findByText('Something went wrong while rendering this PDF.')
      cy.findByLabelText('Page 1').should('not.exist')
    })

    it('shows an error for a corrupt PDF', function () {
      cy.intercept('/build/*/output.pdf?*', {
        fixture: 'build/output-corrupt.pdf,null',
      }).as('compile-pdf-corrupt')

      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      cy.wait('@compile-pdf-corrupt')

      cy.findByText('Something went wrong while rendering this PDF.')
      cy.findByLabelText('Page 1').should('not.exist')
    })
  })

  describe('human readable logs', function () {
    it('shows human readable hint for undefined reference errors', function () {
      cy.intercept('/build/*/output.log?*', {
        fixture: 'build/output-human-readable.log',
      }).as('log')

      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      cy.wait('@log')
      cy.findByRole('button', { name: 'View logs' }).click()

      cy.findByText(
        "Reference `intorduction' on page 1 undefined on input line 11."
      )
      cy.findByText(
        "Reference `section1' on page 1 undefined on input line 13."
      )
      cy.findByText('There were undefined references.')

      cy.findAllByText(
        /You have referenced something which has not yet been labelled/
      ).should('have.length', 3)
    })

    it('does not show human readable hint when no undefined reference errors', function () {
      cy.intercept('/build/*/output.log?*', {
        fixture: 'build/output-undefined-references.log',
      }).as('log')

      const scope = mockScope()

      cy.mount(
        <EditorProviders scope={scope}>
          <div className="pdf-viewer">
            <PdfPreview />
          </div>
        </EditorProviders>
      )

      cy.wait('@log')
      cy.findByRole('button', { name: 'View logs' }).click()

      cy.findByText(
        "Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right."
      )

      cy.findByText(
        /You have referenced something which has not yet been labelled/
      ).should('not.exist')
    })
  })
})
