import Debug from 'debug'
import preprocessor from './plugins/preprocessor'
import { SocketBase } from './socket-base'
import { fs } from './util/fs'
import type { DestroyableHttpServer } from './util/server_destroy'
import type { FoundSpec } from '@packages/types'

const debug = Debug('cypress:server:socket-e2e')

const isSpecialSpec = (name) => {
  return name.endsWith('__all')
}

export class SocketE2E extends SocketBase {
  private testFilePath: string | null

  constructor (config: Record<string, any>) {
    super(config)

    this.testFilePath = null

    this.onTestFileChange = this.onTestFileChange.bind(this)

    if (config.watchForFileChanges) {
      preprocessor.emitter.on('file:updated', this.onTestFileChange)
    }
  }

  onBeforeSave (config) {
    // even if the user has turned off file watching
    // we want to force a reload on save
    if (!config.watchForFileChanges) {
      preprocessor.emitter.on('file:updated', this.onCloudTestFileChange)
    }
  }

  onAfterSave (config, error) {
    // even if the user has turned off file watching
    // we want to force a reload on save
    if (error && !config.watchForFileChanges) {
      preprocessor.emitter.off('file:updated', this.onCloudTestFileChange)
    }
  }

  onCloudTestFileChange = (filePath) => {
    // wait for the studio test file to be written to disk, then reload the test
    // and remove the listener (since this handler is only invoked when watchForFileChanges is false)
    return this.onTestFileChange(filePath).then(() => {
      this.removeOnCloudTestFileChange()
    })
  }

  removeOnCloudTestFileChange () {
    return preprocessor.emitter.off('file:updated', this.onCloudTestFileChange)
  }

  onTestFileChange = (filePath) => {
    debug('test file changed %o', filePath)

    return fs.statAsync(filePath)
    .then(() => {
      this._cdpIo?.emit('watched:file:changed')
      this._socketIo?.emit('watched:file:changed')
    }).catch(() => {
      return debug('could not find test file that changed %o', filePath)
    })
  }

  watchTestFileByPath (config, specConfig: FoundSpec) {
    debug('watching spec with config %o', specConfig)

    // previously we have assumed that we pass integration spec path with "integration/" prefix
    // now we pass spec config object that tells what kind of spec it is, has relative path already
    // so the only special handling remains for special paths like "integration/__all"

    // bail if this is special path like "__all"
    // maybe the client should not ask to watch non-spec files?
    if (isSpecialSpec(specConfig.relative)) {
      return
    }

    if (specConfig.relative.startsWith('/')) {
      specConfig.relative = specConfig.relative.slice(1)
    }

    // bail if we're already watching this exact file
    if (specConfig.relative === this.testFilePath) {
      return
    }

    // remove the existing file by its path
    if (this.testFilePath) {
      preprocessor.removeFile(this.testFilePath, config)
    }

    // store this location
    this.testFilePath = specConfig.relative
    debug('will watch test file path %o', specConfig.relative)

    return preprocessor.getFile(specConfig.relative, config)
    // ignore errors b/c we're just setting up the watching. errors
    // are handled by the spec controller
    .catch(() => {})
  }

  startListening (server: DestroyableHttpServer, automation, config, options) {
    return super.startListening(server, automation, config, options, {
      onSocketConnection: (socket) => {
        socket.on('watch:test:file', (specInfo: FoundSpec, cb = function () { }) => {
          debug('watch:test:file %o', specInfo)

          this.watchTestFileByPath(config, specInfo)

          // callback is only for testing purposes
          return cb()
        })
      },
    })
  }

  close () {
    preprocessor.emitter.removeListener('file:updated', this.onTestFileChange)

    return super.close()
  }
}
