/* eslint-disable react-hooks/rules-of-hooks */
import type {
  BrowserContext,
  ElectronApplication,
  Page,
  TestInfo,
} from '@playwright/test'
import { _electron as electron } from '@playwright/test'

import fs from 'node:fs'
import path from 'path'
import { SETTINGS_FILE_NAME } from '@src/lib/constants'
import type { DeepPartial } from '@src/lib/types'
import fsp from 'fs/promises'

import type { Settings } from '@rust/kcl-lib/bindings/Settings'

import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
import { SignInPageFixture } from '@e2e/playwright/fixtures/signInPageFixture'
import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'

import { TEST_SETTINGS } from '@e2e/playwright/storageStates'
import { getUtils, settingsToToml, setup } from '@e2e/playwright/test-utils'

export class AuthenticatedApp {
  public readonly page: Page
  public readonly context: BrowserContext
  public readonly testInfo: TestInfo
  public readonly viewPortSize = { width: 1200, height: 500 }
  public electronApp: undefined | ElectronApplication
  public projectDirName: string = ''

  constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
    this.context = context
    this.page = page
    this.testInfo = testInfo
  }

  async initialise(code = '') {
    const testDir = this.testInfo.outputPath('electron-test-projects-dir')
    await setup(this.context, this.page, testDir, this.testInfo)
    const u = await getUtils(this.page)

    await this.page.addInitScript(async (code) => {
      localStorage.setItem('persistCode', code)
      ;(window as any).playwrightSkipFilePicker = true
    }, code)

    await this.page.setViewportSize(this.viewPortSize)

    await u.waitForAuthSkipAppStart()
  }
  getInputFile = (fileName: string) => {
    return fsp.readFile(
      path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
      'utf-8'
    )
  }
}

export interface Fixtures {
  cmdBar: CmdBarFixture
  editor: EditorFixture
  toolbar: ToolbarFixture
  scene: SceneFixture
  homePage: HomePageFixture
  signInPage: SignInPageFixture
}

export class ElectronZoo {
  public available: boolean = true
  public electron!: ElectronApplication
  public firstUrl = ''
  public viewPortSize = { width: 1200, height: 500 }
  public projectDirName = ''

  public page!: Page
  public context!: BrowserContext

  constructor() {}

  // Help remote end by signaling we're done with the connection.
  // If it takes longer than 10s to stop, just resolve.
  async makeAvailableAgain() {
    await this.page.evaluate(async () => {
      return new Promise((resolve) => {
        if (!window.engineCommandManager.engineConnection?.state?.type) {
          return resolve(undefined)
        }

        window.engineCommandManager.tearDown()

        // Keep polling (per js event tick) until state is Disconnected.
        const timeA = Date.now()
        const checkDisconnected = () => {
          // It's possible we never even created an engineConnection
          // e.g. never left Projects view.
          if (
            window.engineCommandManager?.engineConnection?.state.type ===
            'disconnected'
          ) {
            return resolve(undefined)
          }

          if (Date.now() - timeA > 3000) {
            return resolve(undefined)
          }

          setTimeout(checkDisconnected, 1)
        }
        checkDisconnected()
      })
    })

    await this.context.tracing.stopChunk({ path: 'trace.zip' })

    // Only after cleanup we're ready.
    this.available = true
  }

  async createInstanceIfMissing(testInfo: TestInfo) {
    // Create or otherwise clear the folder.
    this.projectDirName = testInfo.outputPath('electron-test-projects-dir')

    // We need to expose this in order for some tests that require folder
    // creation and some code below.
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const that = this

    const options = {
      args: ['.', '--no-sandbox'],
      env: {
        ...process.env,
        IS_PLAYWRIGHT: 'true',
      },
      ...(process.env.ELECTRON_OVERRIDE_DIST_PATH
        ? {
            executablePath:
              process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron',
          }
        : {}),
      ...(process.env.PLAYWRIGHT_RECORD_VIDEO
        ? {
            recordVideo: {
              dir: testInfo.snapshotPath(),
              size: this.viewPortSize,
            },
          }
        : {}),
    }

    // Do this once and then reuse window on subsequent calls.
    if (!this.electron) {
      this.electron = await electron.launch(options)

      // Mac takes quite a long time to create the first window in CI.
      // Turns out we can't trust firstWindow() either. So loop.
      let timeoutId: ReturnType<typeof setTimeout>
      const tryToGetWindowPage = () =>
        new Promise((resolve) => {
          const fn = () => {
            this.page = this.electron.windows()[0]
            timeoutId = setTimeout(() => {
              if (this.page) {
                clearTimeout(timeoutId)
                return resolve(undefined)
              }
              fn()
            }, 0)
          }
          fn()
        })

      await tryToGetWindowPage()

      this.context = this.electron.context()
      await this.context.tracing.start({ screenshots: true, snapshots: true })

      // We need to patch this because addInitScript will bind too late in our
      // electron tests, never running. We need to call reload() after each call
      // to guarantee it runs.
      const oldContextAddInitScript = this.context.addInitScript
      this.context.addInitScript = async function (a, b) {
        // @ts-ignore pretty sure way out of tsc's type checking capabilities.
        // This code works perfectly fine.
        await oldContextAddInitScript.apply(this, [a, b])
        await that.page.reload()
      }

      const oldPageAddInitScript = this.page.addInitScript
      this.page.addInitScript = async function (a: any, b: any) {
        // @ts-ignore pretty sure way out of tsc's type checking capabilities.
        // This code works perfectly fine.
        await oldPageAddInitScript.apply(this, [a, b])
        await that.page.reload()
      }
    }

    await this.context.tracing.startChunk()

    // THIS IS ABSOLUTELY NECESSARY TO CHANGE THE PROJECT DIRECTORY BETWEEN
    // TESTS BECAUSE OF THE ELECTRON INSTANCE REUSE.
    await this.electron?.evaluate(({ app }, projectDirName) => {
      // @ts-ignore can't declaration merge see main.ts
      app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
    }, this.projectDirName)

    await setup(this.context, this.page, this.projectDirName, testInfo)

    await this.cleanProjectDir()

    // Create a consistent way to resize the page across electron and web.
    // (lee) I had to do everything in the book to make electron change its
    // damn window size. I succeeded in making it consistently and reliably
    // do it after a whole afternoon.
    this.page.setBodyDimensions = async function (dims: {
      width: number
      height: number
    }) {
      await this.setViewportSize(dims)

      await that.electron?.evaluateHandle(async ({ app }, dims) => {
        // @ts-ignore sorry jon but see comment in main.ts why this is ignored
        await app.resizeWindow(dims.width, dims.height)
      }, dims)

      return this.evaluate(async (dims: { width: number; height: number }) => {
        await window.electron.resizeWindow(dims.width, dims.height)
        window.document.body.style.width = dims.width + 'px'
        window.document.body.style.height = dims.height + 'px'
        window.document.documentElement.style.width = dims.width + 'px'
        window.document.documentElement.style.height = dims.height + 'px'
      }, dims)
    }

    await this.page.setBodyDimensions(this.viewPortSize)

    this.context.folderSetupFn = async function (fn) {
      return fn(that.projectDirName)
        .then(() => that.page.reload())
        .then(() => ({
          dir: that.projectDirName,
        }))
    }

    if (!this.firstUrl) {
      await this.page.getByRole('heading', { name: 'Projects' }).count()
      this.firstUrl = this.page.url()
    }

    // Due to the app controlling its own window context we need to inject new
    // options and context here.
    // NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
    // await tronApp.electronApp.evaluate(({ app }) => {
    //   return app.reuseWindowForTest();
    // });

    // Always start at the root view
    await this.page.goto(this.firstUrl)

    // Force a hard reload, destroying the stream and other state
    await this.page.reload()
  }

  async cleanProjectDir(appSettings?: DeepPartial<Settings>) {
    try {
      if (fs.existsSync(this.projectDirName)) {
        await fsp.rm(this.projectDirName, { recursive: true })
      }
    } catch (_e) {
      console.error(_e)
    }

    try {
      await fsp.mkdir(this.projectDirName)
    } catch (error: unknown) {
      void error
      // Not a problem if it already exists.
    }

    const tempSettingsFilePath = path.resolve(
      this.projectDirName,
      '..',
      SETTINGS_FILE_NAME
    )

    let settingsOverridesToml = ''

    if (appSettings) {
      settingsOverridesToml = settingsToToml({
        settings: {
          ...TEST_SETTINGS,
          ...appSettings,
          app: {
            ...TEST_SETTINGS.app,
            ...appSettings.app,
          },
          project: {
            ...TEST_SETTINGS.project,
            directory: this.projectDirName,
          },
        },
      })
    } else {
      settingsOverridesToml = settingsToToml({
        settings: {
          ...TEST_SETTINGS,
          app: {
            ...TEST_SETTINGS.app,
          },
          project: {
            ...TEST_SETTINGS.project,
            directory: this.projectDirName,
          },
        },
      })
    }
    await fsp.writeFile(tempSettingsFilePath, settingsOverridesToml)
  }
}

// If yee encounter this, please try to type it.
type FnUse = any

const fixturesForElectron = {
  page: async (
    { tronApp }: { tronApp: ElectronZoo },
    use: FnUse,
    testInfo: TestInfo
  ) => {
    await use(tronApp.page)
  },
  context: async (
    { tronApp }: { tronApp: ElectronZoo },
    use: FnUse,
    testInfo: TestInfo
  ) => {
    await use(tronApp.context)
  },
}

const fixturesForWeb = {
  page: async (
    { page, context }: { page: Page; context: BrowserContext },
    use: FnUse,
    testInfo: TestInfo
  ) => {
    page.setBodyDimensions = page.setViewportSize

    // We do the same thing in ElectronZoo. addInitScript simply doesn't fire
    // at the correct time, so we reload the page and it fires appropriately.
    const oldPageAddInitScript = page.addInitScript
    page.addInitScript = async function (...args) {
      // @ts-expect-error
      await oldPageAddInitScript.apply(this, args)
      await page.reload()
    }

    const oldContextAddInitScript = context.addInitScript
    context.addInitScript = async function (...args) {
      // @ts-expect-error
      await oldContextAddInitScript.apply(this, args)
      await page.reload()
    }

    const webApp = new AuthenticatedApp(context, page, testInfo)
    await webApp.initialise()

    await use(page)
  },
}

const fixturesBasedOnProcessEnvPlatform = {
  cmdBar: async ({ page }: { page: Page }, use: FnUse) => {
    await use(new CmdBarFixture(page))
  },
  editor: async ({ page }: { page: Page }, use: FnUse) => {
    await use(new EditorFixture(page))
  },
  toolbar: async ({ page }: { page: Page }, use: FnUse) => {
    await use(new ToolbarFixture(page))
  },
  scene: async ({ page }: { page: Page }, use: FnUse) => {
    await use(new SceneFixture(page))
  },
  homePage: async ({ page }: { page: Page }, use: FnUse) => {
    await use(new HomePageFixture(page))
  },
  signInPage: async ({ page }: { page: Page }, use: FnUse) => {
    await use(new SignInPageFixture(page))
  },
}

if (process.env.PLATFORM === 'web') {
  Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForWeb)
} else {
  Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForElectron)
}

export { fixturesBasedOnProcessEnvPlatform }
