import { Buffer } from 'node:buffer'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { build } from 'vite'
import { createRouteIndex } from '../../utils/routeIndex'
import { createVirtualEntry } from './virtualEntryPlugin'

// mock isNativeEnvironment
vi.mock('vxrn', () => ({
  isNativeEnvironment: (env: any) => {
    return env?.name === 'ios' || env?.name === 'android'
  },
}))

vi.mock('@vxrn/compiler', () => ({
  configuration: { enableNativewind: false },
}))

function loadEntry(
  plugin: any,
  envName: string,
  command: 'serve' | 'build' = 'build',
  envMode: 'dev' | 'build' = command === 'serve' ? 'dev' : 'build'
) {
  // simulate configResolved so isDevMode is set
  plugin.configResolved?.({ root: '', command })
  const ctx = {
    environment: { name: envName, mode: envMode },
  }
  return plugin.load.handler.call(ctx, '\0virtual:one-entry')
}

describe('virtualEntryPlugin', () => {
  it('keeps spa route matching without bundling the client module on the server', async () => {
    const root = mkdtempSync(join(tmpdir(), 'one-spa-server-'))
    const appDir = join(root, 'app')
    mkdirSync(appDir)
    const spaRoute = join(appDir, 'client-only+spa.tsx')
    writeFileSync(
      spaRoute,
      `export const browserPayload = 'must-not-enter-server-graph'
export default function ClientOnlyPage() { return null }
`
    )

    try {
      const result = await build({
        root,
        configFile: false,
        logLevel: 'silent',
        plugins: [
          {
            name: 'test-one-entry',
            enforce: 'pre',
            resolveId(id) {
              if (id === 'one') return '\0test-one-entry'
            },
            load(id) {
              if (id === '\0test-one-entry') {
                return `export const createApp = (options) => options
export const registerPreloadedRoute = () => {}`
              }
            },
          },
          createVirtualEntry({
            root: 'app',
            flags: {},
            routeIndex: createRouteIndex({ routerRoot: appDir }),
          }),
        ],
        build: {
          ssr: true,
          write: false,
          rolldownOptions: {
            input: 'virtual:one-entry',
          },
        },
      })
      if (Array.isArray(result)) throw new Error('expected one server build output')
      if (!('output' in result)) throw new Error('expected completed server build')

      const moduleIds = result.output.flatMap((output) =>
        output.type === 'chunk' ? output.moduleIds : []
      )
      expect(moduleIds).not.toContain(resolve(spaRoute))
      expect(
        result.output
          .flatMap((output) => (output.type === 'chunk' ? output.code : ''))
          .join('\n')
      ).not.toContain('must-not-enter-server-graph')

      const entry = result.output.find(
        (output) => output.type === 'chunk' && output.isEntry
      )
      if (!entry || entry.type !== 'chunk') throw new Error('missing server entry chunk')
      const built = await import(
        `data:text/javascript;base64,${Buffer.from(entry.code).toString('base64')}`
      )
      expect(Object.keys(built.default.routes)).toContain('/app/client-only+spa.tsx')
      await expect(built.default.routes['/app/client-only+spa.tsx']()).resolves.toEqual(
        {}
      )
    } finally {
      rmSync(root, { recursive: true, force: true })
    }
  })

  describe('setupFile', () => {
    const base = {
      root: 'app',
      flags: {},
      routeIndex: {
        getPaths: () => [],
        update: () => false,
      },
    }

    it('server (ssr) uses lazy dynamic import, not static import', () => {
      const plugin = createVirtualEntry({
        ...base,
        setupFile: {
          client: './src/setupClient.ts',
          server: './src/setupServer.ts',
        },
      })
      const code = loadEntry(plugin, 'ssr')
      // must NOT use static import (executes at build time)
      expect(code).not.toContain('import "./src/setupServer.ts"')
      // must use lazy function wrapping dynamic import
      expect(code).toContain('__oneGetSetupPromise = () => import(')
      expect(code).toContain('./src/setupServer.ts')
      // must pass it to createApp so it's called at render time
      expect(code).toContain('getSetupPromise: __oneGetSetupPromise')
    })

    it('client uses dynamic import for setupFile', () => {
      const plugin = createVirtualEntry({
        ...base,
        setupFile: {
          client: './src/setupClient.ts',
          server: './src/setupServer.ts',
        },
      })
      const code = loadEntry(plugin, 'client')
      // should have lazy dynamic import
      expect(code).toContain('__oneGetSetupPromise')
      expect(code).toContain('import(')
      expect(code).toContain('./src/setupClient.ts')
      expect(code).toContain('getSetupPromise: __oneGetSetupPromise')
      // should NOT have a static import
      expect(code).not.toContain('import "./src/setupClient.ts"')
    })

    it('native (ios) uses static import for setupFile', () => {
      const plugin = createVirtualEntry({
        ...base,
        setupFile: {
          client: './src/setupClient.ts',
          native: './src/setupNative.ts',
        },
      })
      const code = loadEntry(plugin, 'ios')
      expect(code).toContain('import "./src/setupNative.ts"')
      // native should NOT use lazy import or pass getSetupPromise
      expect(code).not.toContain('__oneGetSetupPromise')
      expect(code).not.toContain('getSetupPromise')
    })

    it('android uses static import like ios', () => {
      const plugin = createVirtualEntry({
        ...base,
        setupFile: {
          native: './src/setupNative.ts',
        },
      })
      const code = loadEntry(plugin, 'android')
      expect(code).toContain('import "./src/setupNative.ts"')
      expect(code).not.toContain('__oneGetSetupPromise')
    })

    it('server (ssr) in dev mode uses static import for dep discovery', () => {
      const plugin = createVirtualEntry({
        ...base,
        setupFile: {
          client: './src/setupClient.ts',
          server: './src/setupServer.ts',
        },
      })
      const code = loadEntry(plugin, 'ssr', 'serve')
      // dev mode SSR uses static import so Vite crawls deps before first request
      expect(code).toContain('import "./src/setupServer.ts"')
      expect(code).not.toContain('__oneGetSetupPromise')
    })

    it('no setupFile produces no import', () => {
      const plugin = createVirtualEntry(base)
      const code = loadEntry(plugin, 'ssr')
      expect(code).not.toContain('setupServer')
      expect(code).not.toContain('__oneGetSetupPromise')
      expect(code).not.toContain('getSetupPromise')
    })

    it('string setupFile applies to all environments', () => {
      const plugin = createVirtualEntry({
        ...base,
        setupFile: './src/setup.ts',
      })
      const ssrCode = loadEntry(plugin, 'ssr')
      const clientCode = loadEntry(plugin, 'client')
      const iosCode = loadEntry(plugin, 'ios')
      // client and ssr both use lazy dynamic import
      expect(ssrCode).toContain('./src/setup.ts')
      expect(ssrCode).toContain('__oneGetSetupPromise')
      expect(ssrCode).not.toContain('import "./src/setup.ts"')
      expect(clientCode).toContain('./src/setup.ts')
      expect(clientCode).toContain('__oneGetSetupPromise')
      expect(clientCode).not.toContain('import "./src/setup.ts"')
      // native uses static import
      expect(iosCode).toContain('import "./src/setup.ts"')
      expect(iosCode).not.toContain('__oneGetSetupPromise')
    })

    it('passes router linking config to createApp', () => {
      const plugin = createVirtualEntry({
        ...base,
        router: {
          linking: {
            scheme: 'threepunchconvo',
            prefixes: ['threepunchconvo://app'],
          },
        },
      })
      const code = loadEntry(plugin, 'ios')

      expect(code).toContain(
        'linking: {"scheme":"threepunchconvo","prefixes":["threepunchconvo://app"]}'
      )
    })
  })

  describe('react-refresh preamble', () => {
    const base = {
      root: 'app',
      flags: {},
      routeIndex: {
        getPaths: () => [],
        update: () => false,
      },
    }

    // /@one/dev.js also installs the preamble, but it is a deferred module
    // script and the client entry is async, so on a large prerendered document
    // the entry wins and every compiler-wrapped route module throws "React
    // refresh preamble was not loaded". the entry has to install it itself so
    // the result does not depend on which script arrives first.
    it('dev client installs the preamble before route modules load', () => {
      const code = loadEntry(createVirtualEntry(base), 'client', 'serve')
      expect(code).toContain(`from '/@react-refresh'`)
      expect(code).toContain('window.$RefreshReg$ = () => {}')
      // must be guarded so it is a no-op when /@one/dev.js already won the race
      expect(code).toContain(`!window.$RefreshReg$`)
      // and it has to sit in the entry body, ahead of the lazily-globbed routes
      expect(code.indexOf('__oneInjectRefresh(window)')).toBeLessThan(
        code.indexOf('createApp(')
      )
    })

    it('production build emits no reference to the dev-only refresh runtime', () => {
      const code = loadEntry(createVirtualEntry(base), 'client', 'build')
      // /@react-refresh only exists while serving, so a build that imported it
      // would ship a broken specifier
      expect(code).not.toContain('/@react-refresh')
      expect(code).not.toContain('$RefreshReg$')
    })

    it('server and native entries never get the preamble', () => {
      for (const envName of ['ssr', 'ios', 'android']) {
        const code = loadEntry(createVirtualEntry(base), envName, 'serve')
        expect(code).not.toContain('/@react-refresh')
        expect(code).not.toContain('$RefreshReg$')
      }
    })
  })
})
