// @vitest-environment jsdom
import { cleanup, render, waitFor } from '@testing-library/react';
import {
  afterAll,
  afterEach,
  beforeAll,
  describe,
  expect,
  it,
  vi,
} from 'vitest';

import {
  type GradientUniformKey,
  gradientPresets,
  gradientUniformKeys,
} from '../../hooks/useGradientController';
import GradientBackground from '../GradientBackground';
import { defaultGradientColors } from '../gradientConfig';

interface DevToolsWindow extends Window {
  __SUNO_DEV__?: {
    devToolsVisible?: boolean;
  };
}

class MockMotionValue {
  private listeners = new Set<(value: number) => void>();

  constructor(private current: number) {}

  on(event: string, handler: (value: number) => void) {
    if (event !== 'change') {
      return () => {};
    }
    this.listeners.add(handler);
    return () => {
      this.listeners.delete(handler);
    };
  }

  get() {
    return this.current;
  }

  set(value: number) {
    this.current = value;
    this.listeners.forEach((listener) => listener(value));
  }
}

interface WorkerMessage {
  type: string;
  [key: string]: unknown;
}

class MockWorker {
  static instances: MockWorker[] = [];
  messages: WorkerMessage[] = [];
  onMessage: ((event: { data: unknown }) => void) | null = null;

  constructor() {
    MockWorker.instances.push(this);
  }

  postMessage(message: WorkerMessage) {
    this.messages.push(message);
  }

  addEventListener(_type: string, handler: (event: { data: unknown }) => void) {
    this.onMessage = handler;
  }

  removeEventListener() {}

  terminate() {}
}

class MockResizeObserver {
  constructor(private readonly _callback: ResizeObserverCallback) {}
  observe() {}
  disconnect() {}
}

class MockOffscreenCanvas {}

const buildController = (initialFlowSpeed: number) => {
  const initialUniforms = {
    vignetteRadius: 1.2,
    vignettePower: 1.6,
    colorScale: 0.3,
    flowWarp: 1,
    flowFrequency: 4,
    flowAmplitude: 12,
    flowSpeed: initialFlowSpeed,
    // Color properties for four corners
    ...defaultGradientColors,
  };

  const motionValues = Object.fromEntries(
    gradientUniformKeys.map((key) => [
      key,
      new MockMotionValue(initialUniforms[key]),
    ])
  ) as Record<GradientUniformKey, MockMotionValue>;

  const controller = {
    state: 'Static' as const,
    motionValues,
    getSnapshot: () => ({
      vignetteRadius: motionValues.vignetteRadius.get(),
      vignettePower: motionValues.vignettePower.get(),
      colorScale: motionValues.colorScale.get(),
      flowWarp: motionValues.flowWarp.get(),
      flowFrequency: motionValues.flowFrequency.get(),
      flowAmplitude: motionValues.flowAmplitude.get(),
      flowSpeed: motionValues.flowSpeed.get(),
      colorTopLeftR: motionValues.colorTopLeftR.get(),
      colorTopLeftG: motionValues.colorTopLeftG.get(),
      colorTopLeftB: motionValues.colorTopLeftB.get(),
      colorTopRightR: motionValues.colorTopRightR.get(),
      colorTopRightG: motionValues.colorTopRightG.get(),
      colorTopRightB: motionValues.colorTopRightB.get(),
      colorBottomLeftR: motionValues.colorBottomLeftR.get(),
      colorBottomLeftG: motionValues.colorBottomLeftG.get(),
      colorBottomLeftB: motionValues.colorBottomLeftB.get(),
      colorBottomRightR: motionValues.colorBottomRightR.get(),
      colorBottomRightG: motionValues.colorBottomRightG.get(),
      colorBottomRightB: motionValues.colorBottomRightB.get(),
      backgroundColorR: motionValues.backgroundColorR.get(),
      backgroundColorG: motionValues.backgroundColorG.get(),
      backgroundColorB: motionValues.backgroundColorB.get(),
    }),
    setUniform: vi.fn(),
    setState: vi.fn(),
    clearOverrides: vi.fn(),
    presets: gradientPresets,
  };

  return { controller, motionValues };
};

describe('GradientBackground', () => {
  const originalWorker = (globalThis as any).Worker;
  const originalResizeObserver = (globalThis as any).ResizeObserver;
  const originalOffscreen = (globalThis as any).OffscreenCanvas;
  const originalTransfer = (HTMLCanvasElement.prototype as any)
    .transferControlToOffscreen;

  beforeAll(() => {
    (globalThis as any).Worker = MockWorker as unknown as typeof Worker;
    (globalThis as any).ResizeObserver = MockResizeObserver;
    (globalThis as any).OffscreenCanvas = MockOffscreenCanvas;
    (HTMLCanvasElement.prototype as any).transferControlToOffscreen =
      function transfer() {
        return new MockOffscreenCanvas();
      };
  });

  afterAll(() => {
    (globalThis as any).Worker = originalWorker;
    (globalThis as any).ResizeObserver = originalResizeObserver;
    (globalThis as any).OffscreenCanvas = originalOffscreen;
    if (originalTransfer) {
      (HTMLCanvasElement.prototype as any).transferControlToOffscreen =
        originalTransfer;
    } else {
      delete (HTMLCanvasElement.prototype as any).transferControlToOffscreen;
    }
  });

  afterEach(() => {
    cleanup();
    MockWorker.instances.length = 0;
  });

  it('sends flow updates to the worker and toggles animation activity', async () => {
    const { controller, motionValues } = buildController(0);

    render(
      <GradientBackground
        controller={controller as any}
        enableDebugControls={false}
        className='test-bg'
      />
    );

    await waitFor(() => {
      expect(
        MockWorker.instances[0]?.messages.some((msg) => msg.type === 'init')
      ).toBe(true);
    });

    motionValues.flowSpeed.set(0.5);

    await waitFor(() => {
      const worker = MockWorker.instances[0];
      expect(
        worker?.messages.some(
          (msg) =>
            msg.type === 'set-uniform' &&
            msg.key === 'flowSpeed' &&
            msg.value === 0.5
        )
      ).toBe(true);
    });

    await waitFor(() => {
      const worker = MockWorker.instances[0];
      expect(
        worker?.messages.some(
          (msg) => msg.type === 'set-animation-active' && msg.active === true
        )
      ).toBe(true);
    });

    motionValues.flowSpeed.set(0);

    await waitFor(() => {
      const worker = MockWorker.instances[0];
      const lastAnimationMessage = worker?.messages
        .filter((msg) => msg.type === 'set-animation-active')
        .pop();
      expect(lastAnimationMessage?.active).toBe(false);
    });
  });

  it('initializes debug helpers when dev tools visibility is toggled', async () => {
    const devWindow = window as unknown as DevToolsWindow;
    devWindow.__SUNO_DEV__ = { devToolsVisible: true };
    const appendSpy = vi.spyOn(document.body, 'appendChild');

    const { controller, motionValues } = buildController(0);

    render(
      <GradientBackground
        controller={controller as any}
        enableDebugControls
        className='test-bg'
      />
    );

    await waitFor(() => {
      expect(appendSpy).toHaveBeenCalled();
    });

    window.dispatchEvent(
      new CustomEvent('suno-devtools-toggle', { detail: { visible: false } })
    );

    motionValues.flowSpeed.set(0.3);

    await waitFor(() => {
      const worker = MockWorker.instances[0];
      expect(
        worker?.messages.some(
          (msg) => msg.type === 'set-animation-active' && msg.active === true
        )
      ).toBe(true);
    });

    delete (window as unknown as DevToolsWindow).__SUNO_DEV__;
    appendSpy.mockRestore();
  });
});
