import { readFileSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { rm } from 'node:fs/promises'
import { isWindows } from 'std-env'
import { join } from 'pathe'
import { expect, test } from './test-utils'

const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack'

const fixtureDir = fileURLToPath(new URL('../fixtures-temp/hmr', import.meta.url))
const sourceDir = fileURLToPath(new URL('../fixtures/hmr', import.meta.url))

test.use({
  nuxt: {
    rootDir: fixtureDir,
    dev: true,
    setupTimeout: (isWindows ? 360 : 120) * 1000,
    env: { TEST: '1' },
    nuxtConfig: {
      test: true,
      builder: isWebpack ? 'webpack' : 'vite',
    },
  },
})

if (process.env.TEST_ENV === 'built' || isWindows) {
  test.skip('Skipped: HMR tests are skipped on Windows or in built mode', () => {})
} else {
  test.describe.configure({ mode: 'serial' })

  // Load the fixture file
  const indexVue = readFileSync(join(sourceDir, 'pages/index.vue'), 'utf8')

  test('basic HMR functionality', async ({ page, goto }) => {
    // Navigate to the page
    writeFileSync(join(fixtureDir, 'pages/index.vue'), indexVue)
    await goto('/')

    // Check initial state
    await expect(page).toHaveTitle('HMR fixture')
    await expect(page.locator('[data-testid="count"]')).toHaveText('1')

    // Test reactivity
    await page.locator('button').click()
    await expect(page.locator('[data-testid="count"]')).toHaveText('2')

    // Modify the file and check for HMR updates
    let newContents = indexVue
      .replace('<Title>HMR fixture</Title>', '<Title>HMR fixture HMR</Title>')
      .replace('<h1>Home page</h1>', '<h1>Home page - but not as you knew it</h1>')
    newContents += '<style scoped>\nh1 { color: red }\n</style>'

    writeFileSync(join(fixtureDir, 'pages/index.vue'), newContents)

    // Wait for the title to be updated via HMR
    await expect(page).toHaveTitle('HMR fixture HMR')

    // Check content HMR
    const h1 = page.locator('h1')
    await expect(h1).toHaveText('Home page - but not as you knew it')

    // Check style HMR
    const h1Color = await h1.evaluate(el => window.getComputedStyle(el).getPropertyValue('color'))
    expect(h1Color).toBe('rgb(255, 0, 0)')

    expect(page).toHaveNoErrorsOrWarnings()
  })

  test('detecting new routes', async ({ fetch }) => {
    // Try accessing a non-existent route
    await rm(join(fixtureDir, 'pages/some-404.vue'), { force: true })
    const res = await fetch('/some-404')
    expect(res.status).toBe(404)

    // Create a new page file
    writeFileSync(join(fixtureDir, 'pages/some-404.vue'), indexVue)

    // Wait for the new route to be available
    await expect(() => fetch('/some-404').then(r => r.status).catch(() => false)).toBeWithPolling(200)
  })

  test('hot reloading route rules', async ({ fetch }) => {
    // Check the initial header
    const file = readFileSync(join(sourceDir, 'pages/route-rules.vue'), 'utf8')
    writeFileSync(join(fixtureDir, 'pages/route-rules.vue'), file)

    await expect(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null)).toBeWithPolling('added in routeRules')

    await new Promise(resolve => setTimeout(resolve, 100))

    // Modify the route rules
    writeFileSync(join(fixtureDir, 'pages/route-rules.vue'), file.replace('added in routeRules', 'edited in dev'))

    // Wait for the route rule to be hot reloaded
    await expect(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null)).toBeWithPolling('edited in dev')
  })

  test('HMR for island components', async ({ page, goto }) => {
    // Navigate to the page with the island components
    await goto('/server-component')

    const componentPath = join(fixtureDir, 'components/islands/HmrComponent.vue')
    const componentContents = readFileSync(componentPath, 'utf8')

    // Test initial state of the component
    await expect(page.getByTestId('hmr-id')).toHaveText('0')

    // Function to update the component and check for changes
    const triggerHmr = (number: string) => writeFileSync(componentPath, componentContents.replace('ref(0)', `ref(${number})`))

    // First edit
    triggerHmr('1')
    await expect(page.getByTestId('hmr-id')).toHaveText('1', { timeout: 10000 })

    // Second edit to make sure HMR is working consistently
    triggerHmr('2')
    await expect(page.getByTestId('hmr-id')).toHaveText('2', { timeout: 10000 })

    expect(page).toHaveNoErrorsOrWarnings()
  })

  // Skip if using webpack since this test only works with Vite
  if (!isWebpack) {
    test('HMR for page meta', async ({ page, goto }) => {
      const pageContents = readFileSync(join(sourceDir, 'pages/page-meta.vue'), 'utf8')
      writeFileSync(join(fixtureDir, 'pages/page-meta.vue'), pageContents)

      await goto('/page-meta')

      // Check initial meta state
      await expect(page.getByTestId('meta')).toHaveText(JSON.stringify({ some: 'stuff' }, null, 2))

      // Update the meta
      writeFileSync(join(fixtureDir, 'pages/page-meta.vue'), pageContents.replace(`some: 'stuff'`, `some: 'other stuff'`))

      // Check if meta updates
      await expect(page.getByTestId('meta')).toHaveText(JSON.stringify({ some: 'other stuff' }, null, 2))

      // Verify no errors in console
      expect(page).toHaveNoErrorsOrWarnings()
    })

    test('HMR for routes', async ({ page, goto }) => {
      await goto('/routes')

      // Create a new route that doesn't exist yet
      writeFileSync(
        join(fixtureDir, 'pages/routes/non-existent.vue'),
        `<template><div data-testid="contents">A new route!</div></template>`,
      )

      // Track console logs
      const consoleLogs: Array<{ type: string, text: string }> = []
      page.on('console', (msg) => {
        consoleLogs.push({
          type: msg.type(),
          text: msg.text(),
        })
      })

      // Wait for HMR to process the new route
      await expect(() => consoleLogs.some(log => log.text.includes('hmr'))).toBeWithPolling(true)

      // Navigate to the new route
      await page.locator('a[href="/routes/non-existent"]').click()

      // Verify the new route content is rendered
      await expect(page.getByTestId('contents')).toHaveText('A new route!')

      // Filter expected warnings about route not existing before the update
      const filteredLogs = consoleLogs.filter(log => (log.type === 'warning' || log.type === 'error') && !log.text.includes('No match found for location with path "/routes/non-existent"'))

      // Verify no unexpected errors
      expect(filteredLogs).toStrictEqual([])
    })
  }
}