import { expect, it, describe, beforeEach, vi } from 'vitest'
import EventEmitter from 'events'
import path from 'path'
import debug from 'debug'
import { IgnorePlugin } from 'webpack'
import { WebpackDevServerConfig } from '../src/devServer'
import { CYPRESS_WEBPACK_ENTRYPOINT, makeWebpackConfig } from '../src/makeWebpackConfig'
import { createModuleMatrixResult } from './test-helpers/createModuleMatrixResult'
import type { SourceRelativeWebpackResult } from '../src/helpers/sourceRelativeWebpackModules'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'

const WEBPACK_DEV_SERVER_VERSIONS: (5)[] = [5]

describe('makeWebpackConfig', () => {
  it('ignores userland webpack `output.publicPath` and `devServer.overlay` with webpack-dev-server v5', async () => {
    const devServerConfig: WebpackDevServerConfig = {
      specs: [],
      cypressConfig: {
        isTextTerminal: false,
        projectRoot: '.',
        supportFile: '/support.js',
        devServerPublicPathRoute: '/test-public-path', // This will be overridden by makeWebpackConfig.ts
      } as Cypress.PluginConfigOptions,
      webpackConfig: {
        output: {
          publicPath: '/this-will-be-ignored',
        },
        devServer: {
          client: {
            progress: false,
            overlay: true, // This will be overridden by makeWebpackConfig.ts
          },
        },
        optimization: {
          emitOnErrors: false, // This will be overridden by makeWebpackConfig.ts
        },
        devtool: 'eval', // This will be overridden by makeWebpackConfig.ts
      },
      devServerEvents: new EventEmitter(),
    }
    const actual = await makeWebpackConfig({
      devServerConfig,
      sourceWebpackModulesResult: createModuleMatrixResult({
        webpack: 5,
        webpackDevServer: 5,
      }),
    })

    // plugins contain circular deps which cannot be serialized in a snapshot.
    // instead just compare the name and order of the plugins.
    ;(actual as any).plugins = actual.plugins.map((p) => p.constructor.name)

    // these will include paths from the user's local file system, so we should not include them the snapshot
    delete actual.output.path
    delete actual.entry

    expect(actual.output.publicPath).toEqual('/test-public-path/')
    expect(actual).toMatchSnapshot()
  })

  WEBPACK_DEV_SERVER_VERSIONS.forEach((VERSION) => {
    describe(`webpack-dev-server: v${VERSION}`, () => {
      it(`removes entrypoint from merged webpackConfig`, async () => {
        const devServerConfig: WebpackDevServerConfig = {
          specs: [],
          cypressConfig: {
            projectRoot: '.',
            devServerPublicPathRoute: '/test-public-path',
          } as Cypress.PluginConfigOptions,
          webpackConfig: {
            entry: { main: 'src/index.js' },
          },
          devServerEvents: new EventEmitter(),
        }
        const actual = await makeWebpackConfig({
          devServerConfig,
          sourceWebpackModulesResult: createModuleMatrixResult({
            webpack: VERSION,
            webpackDevServer: VERSION,
          }),
        })

        expect(actual.entry).toEqual(CYPRESS_WEBPACK_ENTRYPOINT)
      })

      it(`removes entrypoint from merged webpackConfig`, async () => {
        const devServerConfig: WebpackDevServerConfig = {
          specs: [],
          cypressConfig: {
            projectRoot: '.',
            devServerPublicPathRoute: '/test-public-path',
          } as Cypress.PluginConfigOptions,
          webpackConfig: {
            entry: { main: 'src/index.js' },
          },
          devServerEvents: new EventEmitter(),
        }
        const actual = await makeWebpackConfig({
          devServerConfig,
          sourceWebpackModulesResult: createModuleMatrixResult({
            webpack: VERSION,
            webpackDevServer: VERSION,
          }),
        })

        expect(actual.entry).toEqual(CYPRESS_WEBPACK_ENTRYPOINT)
      })

      it(`preserves entrypoint from merged webpackConfig if framework = angular`, async () => {
        const devServerConfig: WebpackDevServerConfig = {
          specs: [],
          cypressConfig: {
            projectRoot: '.',
            devServerPublicPathRoute: '/test-public-path',
          } as Cypress.PluginConfigOptions,
          webpackConfig: {
            entry: { main: 'src/index.js' },
          },
          devServerEvents: new EventEmitter(),
          framework: 'angular',
        }
        const actual = await makeWebpackConfig({
          devServerConfig,
          sourceWebpackModulesResult: createModuleMatrixResult({
            webpack: VERSION,
            webpackDevServer: VERSION,
          }),
        })

        expect(actual.entry).toEqual({
          main: 'src/index.js',
          'cypress-entry': CYPRESS_WEBPACK_ENTRYPOINT,
        })
      })

      describe('config resolution', () => {
        it('with <project-root>/webpack.config.js', async () => {
          const devServerConfig: WebpackDevServerConfig = {
            specs: [],
            cypressConfig: {
              projectRoot: path.join(__dirname, 'fixtures'),
              devServerPublicPathRoute: '/test-public-path', // This will be overridden by makeWebpackConfig.ts
            } as Cypress.PluginConfigOptions,
            devServerEvents: new EventEmitter(),
          }

          const actual = await makeWebpackConfig({
            devServerConfig,
            sourceWebpackModulesResult: createModuleMatrixResult({
              webpack: 5,
              webpackDevServer: VERSION,
            }),
          })

          expect(actual.plugins.map((p) => p.constructor.name).sort()).toEqual(
            ['CypressCTWebpackPlugin', 'FromWebpackConfigFile', 'HtmlWebpackPlugin'],
          )
        })

        it('with component.devServer.webpackConfig', async () => {
          class FromInlineWebpackConfig {
            apply () {}
          }

          const devServerConfig: WebpackDevServerConfig = {
            specs: [],
            cypressConfig: {
              projectRoot: path.join(__dirname, 'fixtures'),
              devServerPublicPathRoute: '/test-public-path', // This will be overridden by makeWebpackConfig.ts
            } as Cypress.PluginConfigOptions,
            devServerEvents: new EventEmitter(),
            webpackConfig: {
              plugins: [new FromInlineWebpackConfig()],
            },
          }

          const actual = await makeWebpackConfig({
            devServerConfig,
            sourceWebpackModulesResult: createModuleMatrixResult({
              webpack: 5,
              webpackDevServer: VERSION,
            }),
          })

          expect(actual.plugins.map((p) => p.constructor.name).sort()).toEqual(
            ['CypressCTWebpackPlugin', 'FromInlineWebpackConfig', 'HtmlWebpackPlugin'],
          )
        })

        it('calls webpackConfig if it is a function, passing in the base config', async () => {
          const testPlugin = new IgnorePlugin({
            contextRegExp: /aaa/,
            resourceRegExp: /bbb/,
          })

          const modifyConfig = vi.fn().mockImplementation(async () => {
            return {
              plugins: [testPlugin],
            }
          })

          const devServerConfig: WebpackDevServerConfig = {
            specs: [],
            cypressConfig: {
              isTextTerminal: false,
              projectRoot: '.',
              supportFile: '/support.js',
              devServerPublicPathRoute: '/test-public-path', // This will be overridden by makeWebpackConfig.ts
            } as Cypress.PluginConfigOptions,
            webpackConfig: modifyConfig,
            devServerEvents: new EventEmitter(),
          }

          const actual = await makeWebpackConfig({
            devServerConfig,
            sourceWebpackModulesResult: createModuleMatrixResult({
              webpack: VERSION,
              webpackDevServer: VERSION,
            }),
          })

          expect(actual.plugins).toHaveLength(3)
          expect(modifyConfig).toHaveBeenCalled()
          // merged plugins get added at the top of the chain by default
          // should be merged, not overriding existing plugins
          expect(actual.plugins[0].constructor.name).toEqual('IgnorePlugin')
          expect(actual.plugins[1].constructor.name).toEqual('HtmlWebpackPlugin')
          expect(actual.plugins[2].constructor.name).toEqual('CypressCTWebpackPlugin')
        })
      })
    })
  })

  describe('file watching', () => {
    let sourceWebpackModulesResult: SourceRelativeWebpackResult
    let devServerConfig: WebpackDevServerConfig

    beforeEach(() => {
      devServerConfig = {
        specs: [],
        cypressConfig: {
          projectRoot: '.',
          devServerPublicPathRoute: '/test-public-path',
        } as Cypress.PluginConfigOptions,
        webpackConfig: {
          entry: { main: 'src/index.js' },
        },
        devServerEvents: new EventEmitter(),
      }
    })

    describe('webpack-dev-server v5', () => {
      beforeEach(() => {
        sourceWebpackModulesResult = createModuleMatrixResult({
          webpack: 5,
          webpackDevServer: 5,
        })
      })

      it('is disabled in run mode', async () => {
        devServerConfig.cypressConfig.isTextTerminal = true

        const actual = await makeWebpackConfig({
          devServerConfig,
          sourceWebpackModulesResult,
        })

        expect(actual.watchOptions.ignored).toEqual('**/*')
      })

      it('uses defaults in open mode', async () => {
        devServerConfig.cypressConfig.isTextTerminal = false

        const actual = await makeWebpackConfig({
          devServerConfig,
          sourceWebpackModulesResult,
        })

        expect(actual.watchOptions?.ignored).toBeUndefined()
      })
    })
  })

  describe('justInTimeCompile', () => {
    let devServerConfig: WebpackDevServerConfig

    const WEBPACK_MATRIX: {
      webpack: 5
      wds: 5
    }[] = [
      {
        webpack: 5,
        wds: 5,
      },
    ]

    beforeEach(() => {
      devServerConfig = {
        specs: [],
        cypressConfig: {
          projectRoot: '.',
          devServerPublicPathRoute: '/test-public-path',
          justInTimeCompile: true,
          baseUrl: null,
        } as Cypress.PluginConfigOptions,
        webpackConfig: {
          entry: { main: 'src/index.js' },
        },
        devServerEvents: new EventEmitter(),
      }
    })

    WEBPACK_MATRIX.forEach(({ webpack, wds }) => {
      describe(`webpack: v${webpack} with webpack-dev-server v${wds}`, () => {
        describe('run mode', () => {
          beforeEach(() => {
            devServerConfig.cypressConfig.isTextTerminal = true
          })

          it('enables watching', async () => {
            const actual = await makeWebpackConfig({
              devServerConfig,
              sourceWebpackModulesResult: createModuleMatrixResult({
                webpack,
                webpackDevServer: wds,
              }),
            })

            expect(actual.watchOptions?.ignored).toEqual(/node_modules/)
          })
        })
      })
    })
  })

  // Gives users a diagnostic output with webpack-bundle-analyzer to get a visible representation of their webpack bundle, which they can send to us
  // to give us an idea what issues they may be experiencing
  describe('enables webpack-bundle-analyzer if DEBUG=cypress-verbose:webpack-dev-server:bundle-analyzer is set', async () => {
    const WEBPACK_VERSIONS: (5)[] = [5]

    beforeEach(() => {
      debug.enable('cypress-verbose:webpack-dev-server:bundle-analyzer')
    })

    afterEach(() => {
      debug.disable()
    })

    WEBPACK_VERSIONS.forEach((version) => {
      it(`works for webpack v${version}`, async () => {
        const actual = await makeWebpackConfig({
          devServerConfig: {
            specs: [],
            cypressConfig: {
              projectRoot: '.',
              devServerPublicPathRoute: '/test-public-path',
              baseUrl: null,
            } as Cypress.PluginConfigOptions,
            webpackConfig: {
              entry: { main: 'src/index.js' },
            },
            devServerEvents: new EventEmitter(),
          },
          sourceWebpackModulesResult: createModuleMatrixResult({
            webpack: version,
            webpackDevServer: version,
          }),
        })

        expect(actual.plugins).toHaveLength(3)
        expect(actual.plugins[2]).toBeInstanceOf(BundleAnalyzerPlugin)
      })
    })
  })
})
