/* eslint-env jest */
import http from 'http'
import url from 'url'
import fs from 'fs-extra'
import webdriver from 'next-webdriver'
import { join } from 'path'
import {
  findPort,
  launchApp,
  killApp,
  nextBuild,
  nextStart,
  fetchViaHTTP,
  check,
  File,
} from 'next-test-utils'

const appDir = join(__dirname, '..')
const nextConfig = new File(join(appDir, 'next.config.js'))

let app
let appPort

const basePath = '/docs'

const runTests = (isDev) => {
  it('should apply temporary redirect when visited directly for GSSP page', async () => {
    const res = await fetchViaHTTP(
      appPort,
      `${basePath}/gssp-blog/redirect-1`,
      undefined,
      {
        redirect: 'manual',
      }
    )
    expect(res.status).toBe(307)

    const { pathname } = url.parse(res.headers.get('location'))

    expect(pathname).toBe(`${basePath}/404`)
  })

  it('should apply temporary redirect when visited directly basePath false for GSSP page', async () => {
    const res = await fetchViaHTTP(
      appPort,
      `${basePath}/gssp-blog/redirect-1-no-basepath-`,
      undefined,
      {
        redirect: 'manual',
      }
    )
    expect(res.status).toBe(307)

    const text = await res.text()
    expect(text).toEqual(`/404`)

    const parsedUrl = url.parse(res.headers.get('location'))
    expect(parsedUrl.pathname).toBe(`/404`)

    const browser = await webdriver(appPort, `${basePath}`)
    await browser.eval(`next.router.push('/gssp-blog/redirect-1-no-basepath-')`)
    await check(
      () => browser.eval('document.documentElement.innerHTML'),
      /oops not found/
    )

    const parsedUrl2 = url.parse(await browser.eval('window.location.href'))
    expect(parsedUrl2.pathname).toBe('/404')
  })

  it('should apply permanent redirect when visited directly for GSSP page', async () => {
    const res = await fetchViaHTTP(
      appPort,
      `${basePath}/gssp-blog/redirect-permanent`,
      undefined,
      {
        redirect: 'manual',
      }
    )
    expect(res.status).toBe(308)

    const text = await res.text()
    expect(text).toEqual(`${basePath}/404`)

    const { pathname } = url.parse(res.headers.get('location'))

    expect(pathname).toBe(`${basePath}/404`)
    expect(res.headers.get('refresh')).toContain(`url=${basePath}/404`)
  })

  it('should apply statusCode 301 redirect when visited directly for GSSP page', async () => {
    const res = await fetchViaHTTP(
      appPort,
      `${basePath}/gssp-blog/redirect-statusCode-301`,
      undefined,
      {
        redirect: 'manual',
      }
    )
    expect(res.status).toBe(301)

    const text = await res.text()
    expect(text).toEqual(`${basePath}/404`)

    const { pathname } = url.parse(res.headers.get('location'))

    expect(pathname).toBe(`${basePath}/404`)
    expect(res.headers.get('refresh')).toBe(null)
  })

  it('should apply statusCode 303 redirect when visited directly for GSSP page', async () => {
    const res = await fetchViaHTTP(
      appPort,
      `${basePath}/gssp-blog/redirect-statusCode-303`,
      undefined,
      {
        redirect: 'manual',
      }
    )
    expect(res.status).toBe(303)

    const text = await res.text()
    expect(text).toEqual(`${basePath}/404`)

    const { pathname } = url.parse(res.headers.get('location'))

    expect(pathname).toBe(`${basePath}/404`)
    expect(res.headers.get('refresh')).toBe(null)
  })

  it('should apply redirect when fallback GSP page is visited directly (internal dynamic)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/gsp-blog/redirect-dest-_gsp-blog_first`,
      {
        retryWaitHydration: true,
      }
    )

    await browser.waitForElementByCss('#gsp')

    const props = JSON.parse(await browser.elementByCss('#props').text())
    expect(props).toEqual({
      params: {
        post: 'first',
      },
    })
    const initialHref = await browser.eval(() => window.initialHref)
    const { pathname } = url.parse(initialHref)
    expect(pathname).toBe(`${basePath}/gsp-blog/redirect-dest-_gsp-blog_first`)
  })

  if (!isDev) {
    it('should apply redirect when fallback GSP page is visited directly (internal dynamic) 2nd visit', async () => {
      const browser = await webdriver(
        appPort,
        `${basePath}/gsp-blog/redirect-dest-_gsp-blog_first`,
        {
          retryWaitHydration: true,
        }
      )

      await browser.waitForElementByCss('#gsp')

      const props = JSON.parse(await browser.elementByCss('#props').text())
      expect(props).toEqual({
        params: {
          post: 'first',
        },
      })
      const initialHref = await browser.eval(() => window.initialHref)
      const { pathname } = url.parse(initialHref)
      // since it was cached the initial value is now the redirect
      // result
      expect(pathname).toBe(`${basePath}/gsp-blog/first`)
    })
  }

  it('should apply redirect when fallback GSP page is visited directly (internal normal)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/gsp-blog/redirect-dest-_`,
      {
        retryWaitHydration: true,
      }
    )

    await browser.waitForElementByCss('#index')

    const initialHref = await browser.eval(() => window.initialHref)
    const { pathname } = url.parse(initialHref)
    expect(pathname).toBe(`${basePath}/gsp-blog/redirect-dest-_`)
  })

  if (!isDev) {
    it('should apply redirect when fallback GSP page is visited directly (internal normal) 2nd visit', async () => {
      const browser = await webdriver(
        appPort,
        `${basePath}/gsp-blog/redirect-dest-_`,
        {
          retryWaitHydration: true,
        }
      )

      await browser.waitForElementByCss('#index')

      const initialHref = await browser.eval(() => window.initialHref)
      const { pathname } = url.parse(initialHref)
      expect(pathname).toBe(`${basePath}`)
    })
  }

  it('should apply redirect when fallback GSP page is visited directly (external)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/gsp-blog/redirect-dest-_missing`,
      {
        retryWaitHydration: true,
      }
    )

    await check(
      () => browser.eval(() => document.documentElement.innerHTML),
      /oops not found/
    )

    const initialHref = await browser.eval(() => window.initialHref)
    expect(initialHref).toBeFalsy()

    const curUrl = await browser.url()
    const { pathname } = url.parse(curUrl)
    expect(pathname).toBe('/docs/missing')
  })

  it('should apply redirect when fallback GSP page is visited directly (external domain)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/gsp-blog/redirect-dest-external`,
      {
        retryWaitHydration: true,
      }
    )

    await check(
      () => browser.eval(() => document.location.hostname),
      'example.com'
    )

    const initialHref = await browser.eval(() => window.initialHref)
    expect(initialHref).toBeFalsy()
  })

  it('should apply redirect when fallback GSSP page is visited directly (external domain)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/gssp-blog/redirect-dest-external`,
      {
        retryWaitHydration: true,
      }
    )

    await check(
      () => browser.eval(() => document.location.hostname),
      'example.com'
    )

    const initialHref = await browser.eval(() => window.initialHref)
    expect(initialHref).toBeFalsy()

    const res = await fetchViaHTTP(
      appPort,
      `${basePath}/gssp-blog/redirect-dest-external`,
      undefined,
      {
        redirect: 'manual',
      }
    )
    expect(res.status).toBe(307)

    const parsed = url.parse(res.headers.get('location'))
    expect(parsed.hostname).toBe('example.com')
    expect(parsed.pathname).toBe('/')
  })

  it('should apply redirect when GSSP page is navigated to client-side (internal dynamic)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/gssp-blog/redirect-dest-_gssp-blog_first`,
      {
        retryWaitHydration: true,
      }
    )

    await browser.waitForElementByCss('#gssp')

    const props = JSON.parse(await browser.elementByCss('#props').text())
    expect(props).toEqual({
      params: {
        post: 'first',
      },
    })
  })

  it('should apply redirect when GSSP page is navigated to client-side (internal normal)', async () => {
    const browser = await webdriver(appPort, `${basePath}`, {
      retryWaitHydration: true,
    })

    await browser.eval(`(function () {
      window.next.router.push('/gssp-blog/redirect-dest-_another')
    })()`)
    await browser.waitForElementByCss('#another')

    const text = await browser.elementByCss('#another').text()

    expect(text).toEqual('another Page')
  })

  it('should apply redirect when GSSP page is navigated to client-side (external)', async () => {
    const browser = await webdriver(appPort, `${basePath}`, {
      retryWaitHydration: true,
    })

    await browser.eval(`(function () {
      window.next.router.push('/gssp-blog/redirect-dest-_gssp-blog_first')
    })()`)
    await browser.waitForElementByCss('#gssp')

    const props = JSON.parse(await browser.elementByCss('#props').text())

    expect(props).toEqual({
      params: {
        post: 'first',
      },
    })
  })

  it('should apply redirect when GSP page is navigated to client-side (internal)', async () => {
    const browser = await webdriver(appPort, `${basePath}`, {
      retryWaitHydration: true,
    })

    await browser.eval(`(function () {
      window.next.router.push('/gsp-blog/redirect-dest-_another')
    })()`)
    await browser.waitForElementByCss('#another')

    const text = await browser.elementByCss('#another').text()

    expect(text).toEqual('another Page')
  })

  it('should apply redirect when GSP page is navigated to client-side (external)', async () => {
    const browser = await webdriver(appPort, `${basePath}`, {
      retryWaitHydration: true,
    })

    await browser.eval(`(function () {
      window.next.router.push('/gsp-blog/redirect-dest-_gsp-blog_first')
    })()`)
    await browser.waitForElementByCss('#gsp')

    const props = JSON.parse(await browser.elementByCss('#props').text())

    expect(props).toEqual({
      params: {
        post: 'first',
      },
    })
  })

  it('should not replace history of the origin page when GSSP page is navigated to client-side (internal normal)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/another?mark_as=root`,
      {
        retryWaitHydration: true,
      }
    )

    await browser.eval(`(function () {
      window.next.router.push('/')
    })()`)
    await browser.waitForElementByCss('#index')

    await browser.eval(`(function () {
      window.next.router.push('/gssp-blog/redirect-dest-_another')
    })()`)
    await browser.waitForElementByCss('#another')

    await browser.eval(`(function () {
      window.history.back()
    })()`)

    const curUrl = await browser.url()
    const { path } = url.parse(curUrl)
    expect(path).toEqual(`${basePath}`)
  })

  it('should not replace history of the origin page when GSSP page is navigated to client-side (external)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/another?mark_as=root`,
      {
        retryWaitHydration: true,
      }
    )

    await browser.eval(`(function () {
      window.next.router.push('/')
    })()`)
    await browser.waitForElementByCss('#index')

    await browser.eval(`(function () {
      window.next.router.push('/gssp-blog/redirect-dest-_gssp-blog_first')
    })()`)
    await browser.waitForElementByCss('#gssp')

    await browser.eval(`(function () {
      window.history.back()
    })()`)

    const curUrl = await browser.url()
    const { path } = url.parse(curUrl)
    expect(path).toEqual(`${basePath}`)
  })

  it('should not replace history of the origin page when GSP page is navigated to client-side (internal)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/another?mark_as=root`,
      {
        retryWaitHydration: true,
      }
    )

    await browser.eval(`(function () {
      window.next.router.push('/')
    })()`)
    await browser.waitForElementByCss('#index')

    await browser.eval(`(function () {
      window.next.router.push('/gsp-blog/redirect-dest-_another')
    })()`)
    await browser.waitForElementByCss('#another')

    await browser.eval(`(function () {
      window.history.back()
    })()`)

    const curUrl = await browser.url()
    const { path } = url.parse(curUrl)
    expect(path).toEqual(`${basePath}`)
  })

  it('should not replace history of the origin page when GSP page is navigated to client-side (external)', async () => {
    const browser = await webdriver(
      appPort,
      `${basePath}/another?mark_as=root`,
      {
        retryWaitHydration: true,
      }
    )

    await browser.eval(`(function () {
      window.next.router.push('/')
    })()`)
    await browser.waitForElementByCss('#index')

    await browser.eval(`(function () {
      window.next.router.push('/gsp-blog/redirect-dest-_gsp-blog_first')
    })()`)
    await browser.waitForElementByCss('#gsp')

    await browser.eval(`(function () {
      window.history.back()
    })()`)

    const curUrl = await browser.url()
    const { path } = url.parse(curUrl)
    expect(path).toEqual(`${basePath}`)
  })
}

describe('GS(S)P Redirect Support', () => {
  describe('dev mode', () => {
    beforeAll(async () => {
      appPort = await findPort()
      app = await launchApp(appDir, appPort)
    })
    afterAll(() => killApp(app))

    runTests(true)
  })

  describe('production mode', () => {
    beforeAll(async () => {
      await fs.remove(join(appDir, '.next'))
      await nextBuild(appDir)
      appPort = await findPort()
      app = await nextStart(appDir, appPort)
    })
    afterAll(() => killApp(app))

    runTests()
  })

  describe('serverless mode', () => {
    let server

    beforeAll(async () => {
      nextConfig.write(
        `module.exports = {
          basePath: "${basePath}",
        target: 'experimental-serverless-trace'
      }`
      )
      await fs.remove(join(appDir, '.next'))
      await nextBuild(appDir)
      appPort = await findPort()
      app = await nextStart(appDir, appPort)
    })
    afterAll(async () => {
      nextConfig.restore()
      await killApp(app)

      try {
        server.close()
      } catch (err) {
        console.error('failed to close server', err)
      }
    })

    it('should handle redirect in raw serverless mode correctly', async () => {
      server = http.createServer(async (req, res) => {
        try {
          console.log(req.url)
          if (req.url.includes('/gsp-blog')) {
            await require(join(
              appDir,
              '.next/serverless/pages/gsp-blog/[post].js'
            )).render(req, res)
          } else {
            await require(join(
              appDir,
              './.next/serverless/pages/gssp-blog/[post].js'
            )).render(req, res)
          }
        } catch (err) {
          console.error('failed to render', err)
          res.statusCode = 500
          res.end('error')
        }
      })
      const port = await findPort()

      await new Promise((resolve, reject) => {
        server.listen(port, (err) => (err ? reject(err) : resolve()))
      })
      console.log(`Raw serverless server listening at port ${port}`)

      const res1 = await fetchViaHTTP(
        port,
        '/gsp-blog/redirect-dest-_gsp-blog_first',
        undefined,
        {
          redirect: 'manual',
        }
      )
      expect(res1.status).toBe(307)
      const text1 = await res1.text()
      expect(text1).toEqual(`${basePath}/gsp-blog/first`)
      const parsed = url.parse(res1.headers.get('location'), true)
      expect(parsed.pathname).toBe(`${basePath}/gsp-blog/first`)
      expect(parsed.query).toEqual({})
      expect(res1.headers.get('refresh')).toBe(null)

      const res2 = await fetchViaHTTP(
        port,
        '/gsp-blog/redirect-permanent-dest-_gsp-blog_first',
        undefined,
        {
          redirect: 'manual',
        }
      )
      expect(res2.status).toBe(308)
      const text2 = await res2.text()
      expect(text2).toEqual(`${basePath}/gsp-blog/first`)
      expect(res2.headers.get('refresh')).toContain(
        `url=${basePath}/gsp-blog/first`
      )
      const parsed2 = url.parse(res2.headers.get('location'), true)
      expect(parsed2.pathname).toBe(`${basePath}/gsp-blog/first`)
      expect(parsed2.query).toEqual({})

      const res3 = await fetchViaHTTP(
        port,
        '/gssp-blog/redirect-dest-_gssp-blog_first',
        undefined,
        {
          redirect: 'manual',
        }
      )
      expect(res3.status).toBe(307)
      const text3 = await res3.text()
      expect(text3).toEqual(`${basePath}/gssp-blog/first`)
      expect(res3.headers.get('refresh')).toBe(null)
      const parsed3 = url.parse(res3.headers.get('location'), true)
      expect(parsed3.pathname).toBe(`${basePath}/gssp-blog/first`)
      expect(parsed3.query).toEqual({})

      const res4 = await fetchViaHTTP(
        port,
        '/gssp-blog/redirect-permanent-dest-_gssp-blog_first',
        undefined,
        {
          redirect: 'manual',
        }
      )
      expect(res4.status).toBe(308)
      const text4 = await res4.text()
      expect(text4).toEqual(`${basePath}/gssp-blog/first`)
      expect(res4.headers.get('refresh')).toContain(
        `url=${basePath}/gssp-blog/first`
      )
      const parsed4 = url.parse(res4.headers.get('location'), true)
      expect(parsed4.pathname).toBe(`${basePath}/gssp-blog/first`)
      expect(parsed4.query).toEqual({})
    })

    runTests()
  })

  it('should error for redirect during prerendering', async () => {
    await fs.mkdirp(join(appDir, 'pages/invalid'))
    await fs.writeFile(
      join(appDir, 'pages', 'invalid', '[slug].js'),
      `
        export default function Post(props) {
          return "hi"
        }

        export const getStaticProps = ({ params }) => {
          return {
            redirect: {
              permanent: true,
              destination: '/another'
            }
          }
        }

        export const getStaticPaths = () => {
          return {
            paths: ['first', 'second'].map((slug) => ({ params: { slug } })),
            fallback: true,
          }
        }
      `
    )
    const { stdout, stderr } = await nextBuild(appDir, undefined, {
      stdout: true,
      stderr: true,
    })
    const output = stdout + stderr
    await fs.remove(join(appDir, 'pages/invalid'))

    expect(output).toContain(
      '`redirect` can not be returned from getStaticProps during prerendering'
    )
  })
})
