import globOrig from 'glob'
import cheerio from 'cheerio'
import { promisify } from 'util'
import path, { join } from 'path'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { check, fetchViaHTTP, normalizeRegEx, waitFor } from 'next-test-utils'
import webdriver from 'next-webdriver'

const glob = promisify(globOrig)

describe('app-dir static/dynamic handling', () => {
  const isDev = (global as any).isNextDev

  let next: NextInstance

  beforeAll(async () => {
    next = await createNext({
      files: new FileRef(path.join(__dirname, 'app-static')),
      dependencies: {
        react: 'latest',
        'react-dom': 'latest',
      },
    })
  }, 600000)
  afterAll(() => next.destroy())

  if ((global as any).isNextStart) {
    it('should output HTML/RSC files for static paths', async () => {
      const files = (
        await glob('**/*', {
          cwd: join(next.testDir, '.next/server/app'),
        })
      ).filter((file) => file.match(/.*\.(js|html|rsc)$/))

      expect(files).toEqual([
        '(new)/custom/page.js',
        'blog/[author]/[slug]/page.js',
        'blog/[author]/page.js',
        'blog/seb.html',
        'blog/seb.rsc',
        'blog/seb/second-post.html',
        'blog/seb/second-post.rsc',
        'blog/styfle.html',
        'blog/styfle.rsc',
        'blog/styfle/first-post.html',
        'blog/styfle/first-post.rsc',
        'blog/styfle/second-post.html',
        'blog/styfle/second-post.rsc',
        'blog/tim.html',
        'blog/tim.rsc',
        'blog/tim/first-post.html',
        'blog/tim/first-post.rsc',
        'dynamic-no-gen-params-ssr/[slug]/page.js',
        'dynamic-no-gen-params/[slug]/page.js',
        'force-static/[slug]/page.js',
        'force-static/first.html',
        'force-static/first.rsc',
        'force-static/page.js',
        'force-static/second.html',
        'force-static/second.rsc',
        'hooks/use-pathname/[slug]/page.js',
        'hooks/use-pathname/slug.html',
        'hooks/use-pathname/slug.rsc',
        'hooks/use-search-params/[slug]/page.js',
        'ssr-auto/cache-no-store/page.js',
        'ssr-auto/fetch-revalidate-zero/page.js',
        'ssr-forced/page.js',
      ])
    })

    it('should have correct prerender-manifest entries', async () => {
      const manifest = JSON.parse(await next.readFile('.next/prerender-manifest.json'))

      Object.keys(manifest.dynamicRoutes).forEach((key) => {
        const item = manifest.dynamicRoutes[key]

        if (item.dataRouteRegex) {
          item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex)
        }
        if (item.routeRegex) {
          item.routeRegex = normalizeRegEx(item.routeRegex)
        }
      })

      expect(manifest.version).toBe(3)
      expect(manifest.routes).toEqual({
        '/blog/tim': {
          initialRevalidateSeconds: 10,
          srcRoute: '/blog/[author]',
          dataRoute: '/blog/tim.rsc',
        },
        '/blog/seb': {
          initialRevalidateSeconds: 10,
          srcRoute: '/blog/[author]',
          dataRoute: '/blog/seb.rsc',
        },
        '/blog/styfle': {
          initialRevalidateSeconds: 10,
          srcRoute: '/blog/[author]',
          dataRoute: '/blog/styfle.rsc',
        },
        '/blog/tim/first-post': {
          initialRevalidateSeconds: false,
          srcRoute: '/blog/[author]/[slug]',
          dataRoute: '/blog/tim/first-post.rsc',
        },
        '/blog/seb/second-post': {
          initialRevalidateSeconds: false,
          srcRoute: '/blog/[author]/[slug]',
          dataRoute: '/blog/seb/second-post.rsc',
        },
        '/blog/styfle/first-post': {
          initialRevalidateSeconds: false,
          srcRoute: '/blog/[author]/[slug]',
          dataRoute: '/blog/styfle/first-post.rsc',
        },
        '/blog/styfle/second-post': {
          initialRevalidateSeconds: false,
          srcRoute: '/blog/[author]/[slug]',
          dataRoute: '/blog/styfle/second-post.rsc',
        },
        '/hooks/use-pathname/slug': {
          dataRoute: '/hooks/use-pathname/slug.rsc',
          initialRevalidateSeconds: false,
          srcRoute: '/hooks/use-pathname/[slug]',
        },
        '/force-static/first': {
          dataRoute: '/force-static/first.rsc',
          initialRevalidateSeconds: false,
          srcRoute: '/force-static/[slug]',
        },
        '/force-static/second': {
          dataRoute: '/force-static/second.rsc',
          initialRevalidateSeconds: false,
          srcRoute: '/force-static/[slug]',
        },
      })
      expect(manifest.dynamicRoutes).toEqual({
        '/blog/[author]/[slug]': {
          routeRegex: normalizeRegEx('^/blog/([^/]+?)/([^/]+?)(?:/)?$'),
          dataRoute: '/blog/[author]/[slug].rsc',
          fallback: null,
          dataRouteRegex: normalizeRegEx('^/blog/([^/]+?)/([^/]+?)\\.rsc$'),
        },
        '/blog/[author]': {
          dataRoute: '/blog/[author].rsc',
          dataRouteRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)\\.rsc$'),
          fallback: false,
          routeRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)(?:\\/)?$'),
        },
        '/hooks/use-pathname/[slug]': {
          dataRoute: '/hooks/use-pathname/[slug].rsc',
          dataRouteRegex: '^\\/hooks\\/use\\-pathname\\/([^\\/]+?)\\.rsc$',
          fallback: null,
          routeRegex: '^\\/hooks\\/use\\-pathname\\/([^\\/]+?)(?:\\/)?$',
        },
        '/force-static/[slug]': {
          dataRoute: '/force-static/[slug].rsc',
          dataRouteRegex: '^\\/force\\-static\\/([^\\/]+?)\\.rsc$',
          fallback: null,
          routeRegex: '^\\/force\\-static\\/([^\\/]+?)(?:\\/)?$',
        },
      })
    })
  }

  it('should force SSR correctly for headers usage', async () => {
    const res = await fetchViaHTTP(next.url, '/force-static', undefined, {
      headers: {
        Cookie: 'myCookie=cookieValue',
        another: 'header',
      },
    })
    expect(res.status).toBe(200)

    const html = await res.text()
    const $ = cheerio.load(html)

    expect(JSON.parse($('#headers').text())).toIncludeAllMembers(['cookie', 'another'])
    expect(JSON.parse($('#cookies').text())).toEqual([
      {
        name: 'myCookie',
        value: 'cookieValue',
      },
    ])

    const firstTime = $('#now').text()

    if (!(global as any).isNextDev) {
      const res2 = await fetchViaHTTP(next.url, '/force-static')
      expect(res2.status).toBe(200)

      const $2 = cheerio.load(await res2.text())
      expect(firstTime).not.toBe($2('#now').text())
    }
  })

  it('should honor dynamic = "force-static" correctly', async () => {
    const res = await fetchViaHTTP(next.url, '/force-static/first')
    expect(res.status).toBe(200)

    const html = await res.text()
    const $ = cheerio.load(html)

    expect(JSON.parse($('#params').text())).toEqual({ slug: 'first' })
    expect(JSON.parse($('#headers').text())).toEqual([])
    expect(JSON.parse($('#cookies').text())).toEqual([])

    const firstTime = $('#now').text()

    if (!(global as any).isNextDev) {
      const res2 = await fetchViaHTTP(next.url, '/force-static/first')
      expect(res2.status).toBe(200)

      const $2 = cheerio.load(await res2.text())
      expect(firstTime).toBe($2('#now').text())
    }
  })

  it('should honor dynamic = "force-static" correctly (lazy)', async () => {
    const res = await fetchViaHTTP(next.url, '/force-static/random')
    expect(res.status).toBe(200)

    const html = await res.text()
    const $ = cheerio.load(html)

    expect(JSON.parse($('#params').text())).toEqual({ slug: 'random' })
    expect(JSON.parse($('#headers').text())).toEqual([])
    expect(JSON.parse($('#cookies').text())).toEqual([])

    const firstTime = $('#now').text()

    if (!(global as any).isNextDev) {
      const res2 = await fetchViaHTTP(next.url, '/force-static/random')
      expect(res2.status).toBe(200)

      const $2 = cheerio.load(await res2.text())
      expect(firstTime).toBe($2('#now').text())
    }
  })
  // NTL Skip
  it.skip('should handle dynamicParams: false correctly', async () => {
    const validParams = ['tim', 'seb', 'styfle']

    for (const param of validParams) {
      const res = await fetchViaHTTP(next.url, `/blog/${param}`, undefined, {
        redirect: 'manual',
      })
      expect(res.status).toBe(200)
      const html = await res.text()
      const $ = cheerio.load(html)

      expect(JSON.parse($('#params').text())).toEqual({
        author: param,
      })
      expect($('#page').text()).toBe('/blog/[author]')
    }
    const invalidParams = ['timm', 'non-existent']

    for (const param of invalidParams) {
      const invalidRes = await fetchViaHTTP(next.url, `/blog/${param}`, undefined, { redirect: 'manual' })
      expect(invalidRes.status).toBe(404)
      expect(await invalidRes.text()).toContain('page could not be found')
    }
  })

  it('should work with forced dynamic path', async () => {
    for (const slug of ['first', 'second']) {
      const res = await fetchViaHTTP(next.url, `/dynamic-no-gen-params-ssr/${slug}`, undefined, { redirect: 'manual' })
      expect(res.status).toBe(200)
      expect(await res.text()).toContain(`${slug}`)
    }
  })

  it('should work with dynamic path no generateStaticParams', async () => {
    for (const slug of ['first', 'second']) {
      const res = await fetchViaHTTP(next.url, `/dynamic-no-gen-params/${slug}`, undefined, { redirect: 'manual' })
      expect(res.status).toBe(200)
      expect(await res.text()).toContain(`${slug}`)
    }
  })

  it('should handle dynamicParams: true correctly', async () => {
    const paramsToCheck = [
      {
        author: 'tim',
        slug: 'first-post',
      },
      {
        author: 'seb',
        slug: 'second-post',
      },
      {
        author: 'styfle',
        slug: 'first-post',
      },
      {
        author: 'new-author',
        slug: 'first-post',
      },
    ]

    for (const params of paramsToCheck) {
      const res = await fetchViaHTTP(next.url, `/blog/${params.author}/${params.slug}`, undefined, {
        redirect: 'manual',
      })
      expect(res.status).toBe(200)
      const html = await res.text()
      const $ = cheerio.load(html)

      expect(JSON.parse($('#params').text())).toEqual(params)
      expect($('#page').text()).toBe('/blog/[author]/[slug]')
    }
  })
  // NTL Skip
  it.skip('should navigate to static path correctly', async () => {
    const browser = await webdriver(next.url, '/blog/tim')
    await browser.eval('window.beforeNav = 1')

    expect(await browser.eval('document.documentElement.innerHTML')).toContain('/blog/[author]')
    await browser.elementByCss('#author-2').click()

    await check(async () => {
      const params = JSON.parse(await browser.elementByCss('#params').text())
      return params.author === 'seb' ? 'found' : params
    }, 'found')

    expect(await browser.eval('window.beforeNav')).toBe(1)
    await browser.elementByCss('#author-1-post-1').click()

    await check(async () => {
      const params = JSON.parse(await browser.elementByCss('#params').text())
      return params.author === 'tim' && params.slug === 'first-post' ? 'found' : params
    }, 'found')

    expect(await browser.eval('window.beforeNav')).toBe(1)
    await browser.back()

    await check(async () => {
      const params = JSON.parse(await browser.elementByCss('#params').text())
      return params.author === 'seb' ? 'found' : params
    }, 'found')

    expect(await browser.eval('window.beforeNav')).toBe(1)
  })

  it('should ssr dynamically when detected automatically with fetch cache option', async () => {
    const pathname = '/ssr-auto/cache-no-store'
    const initialRes = await fetchViaHTTP(next.url, pathname, undefined, {
      redirect: 'manual',
    })
    expect(initialRes.status).toBe(200)

    const initialHtml = await initialRes.text()
    const initial$ = cheerio.load(initialHtml)

    expect(initial$('#page').text()).toBe(pathname)
    const initialDate = initial$('#date').text()

    expect(initialHtml).toContain('Example Domain')

    const secondRes = await fetchViaHTTP(next.url, pathname, undefined, {
      redirect: 'manual',
    })
    expect(secondRes.status).toBe(200)

    const secondHtml = await secondRes.text()
    const second$ = cheerio.load(secondHtml)

    expect(second$('#page').text()).toBe(pathname)
    const secondDate = second$('#date').text()

    expect(secondHtml).toContain('Example Domain')
    expect(secondDate).not.toBe(initialDate)
  })

  it('should render not found pages correctly and fallback to the default one', async () => {
    const res = await fetchViaHTTP(next.url, `/blog/shu/hi`, undefined, {
      redirect: 'manual',
    })
    expect(res.status).toBe(404)
    const html = await res.text()
    expect(html).toInclude('"noindex"')
    expect(html).toInclude('This page could not be found.')
  })

  // TODO-APP: support fetch revalidate case for dynamic rendering
  it.skip('should ssr dynamically when detected automatically with fetch revalidate option', async () => {
    const pathname = '/ssr-auto/fetch-revalidate-zero'
    const initialRes = await fetchViaHTTP(next.url, pathname, undefined, {
      redirect: 'manual',
    })
    expect(initialRes.status).toBe(200)

    const initialHtml = await initialRes.text()
    const initial$ = cheerio.load(initialHtml)

    expect(initial$('#page').text()).toBe(pathname)
    const initialDate = initial$('#date').text()

    expect(initialHtml).toContain('Example Domain')

    const secondRes = await fetchViaHTTP(next.url, pathname, undefined, {
      redirect: 'manual',
    })
    expect(secondRes.status).toBe(200)

    const secondHtml = await secondRes.text()
    const second$ = cheerio.load(secondHtml)

    expect(second$('#page').text()).toBe(pathname)
    const secondDate = second$('#date').text()

    expect(secondHtml).toContain('Example Domain')
    expect(secondDate).not.toBe(initialDate)
  })

  it('should ssr dynamically when forced via config', async () => {
    const initialRes = await fetchViaHTTP(next.url, '/ssr-forced', undefined, {
      redirect: 'manual',
    })
    expect(initialRes.status).toBe(200)

    const initialHtml = await initialRes.text()
    const initial$ = cheerio.load(initialHtml)

    expect(initial$('#page').text()).toBe('/ssr-forced')
    const initialDate = initial$('#date').text()

    const secondRes = await fetchViaHTTP(next.url, '/ssr-forced', undefined, {
      redirect: 'manual',
    })
    expect(secondRes.status).toBe(200)

    const secondHtml = await secondRes.text()
    const second$ = cheerio.load(secondHtml)

    expect(second$('#page').text()).toBe('/ssr-forced')
    const secondDate = second$('#date').text()

    expect(secondDate).not.toBe(initialDate)
  })

  describe('hooks', () => {
    describe('useSearchParams', () => {
      if (isDev) {
        it('should bail out to client rendering during SSG', async () => {
          const res = await fetchViaHTTP(next.url, '/hooks/use-search-params/slug')
          const html = await res.text()
          expect(html).toInclude('<html id="__next_error__">')
        })
      }

      it('should have the correct values', async () => {
        const browser = await webdriver(next.url, '/hooks/use-search-params/slug?first=value&second=other&third')

        expect(await browser.elementByCss('#params-first').text()).toBe('value')
        expect(await browser.elementByCss('#params-second').text()).toBe('other')
        expect(await browser.elementByCss('#params-third').text()).toBe('')
        expect(await browser.elementByCss('#params-not-real').text()).toBe('N/A')
      })

      // TODO-APP: re-enable after investigating rewrite params
      if (!(global as any).isNextDeploy) {
        it('should have values from canonical url on rewrite', async () => {
          const browser = await webdriver(next.url, '/rewritten-use-search-params?first=a&second=b&third=c')

          expect(await browser.elementByCss('#params-first').text()).toBe('a')
          expect(await browser.elementByCss('#params-second').text()).toBe('b')
          expect(await browser.elementByCss('#params-third').text()).toBe('c')
          expect(await browser.elementByCss('#params-not-real').text()).toBe('N/A')
        })
      }
    })

    // TODO: needs updating as usePathname should not bail
    describe.skip('usePathname', () => {
      if (isDev) {
        it('should bail out to client rendering during SSG', async () => {
          const res = await fetchViaHTTP(next.url, '/hooks/use-pathname/slug')
          const html = await res.text()
          expect(html).toInclude('<html id="__next_error__">')
        })
      }

      it('should have the correct values', async () => {
        const browser = await webdriver(next.url, '/hooks/use-pathname/slug')

        expect(await browser.elementByCss('#pathname').text()).toBe('/hooks/use-pathname/slug')
      })

      it('should have values from canonical url on rewrite', async () => {
        const browser = await webdriver(next.url, '/rewritten-use-pathname')

        expect(await browser.elementByCss('#pathname').text()).toBe('/rewritten-use-pathname')
      })
    })

    if (!(global as any).isNextDeploy) {
      it('should show a message to leave feedback for `appDir`', async () => {
        expect(next.cliOutput).toContain(
          `Thank you for testing \`appDir\` please leave your feedback at https://nextjs.link/app-feedback`,
        )
      })
    }

    it('should keep querystring on static page', async () => {
      const browser = await webdriver(next.url, '/blog/tim?message=hello-world')
      const checkUrl = async () => expect(await browser.url()).toBe(next.url + '/blog/tim?message=hello-world')

      checkUrl()
      await waitFor(1000)
      checkUrl()
    })
  })
})
