/*
 * If not stated otherwise in this file or this component's LICENSE file the
 * following copyright and licenses apply:
 *
 * Copyright 2023 Comcast Cable Communications Management, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the License);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, expect, it, vi, beforeEach } from 'vitest';
import { CoreAnimationController } from './CoreAnimationController.js';
import { AnimationManager } from './AnimationManager.js';
import { CoreAnimation } from './CoreAnimation.js';
import { mock } from 'vitest-mock-extended';
import { EventEmitter } from '../../common/EventEmitter.js';

/**
 * Minimal mock for CoreAnimation that extends EventEmitter so event
 * wiring in registerAnimation / unregisterAnimation works correctly.
 */
function createMockAnimation(
  overrides: Partial<CoreAnimation> = {},
): CoreAnimation {
  const emitter = new EventEmitter();
  const animation = Object.assign(emitter, {
    id: 1,
    duration: 1000,
    delay: 0,
    easing: 'linear',
    loop: false,
    repeat: 0,
    stopMethod: false as const,
    progress: 0,
    propValuesMap: { props: null, shaderProps: null },
    reset: vi.fn(),
    restore: vi.fn(),
    reverse: vi.fn(),
    update: vi.fn(),
    ...overrides,
  }) as unknown as CoreAnimation;

  return animation;
}

describe('CoreAnimationController', () => {
  let manager: AnimationManager;
  let animation: CoreAnimation;

  function createController(
    mgr?: AnimationManager,
    anim?: CoreAnimation,
  ): CoreAnimationController {
    const controller = new CoreAnimationController();
    controller.init(mgr ?? manager, anim ?? animation);
    return controller;
  }

  beforeEach(() => {
    manager = mock<AnimationManager>();
    animation = createMockAnimation();
  });

  describe('constructor', () => {
    it('should initialize in stopped state', () => {
      const controller = createController();
      expect(controller.state).toBe('stopped');
    });
  });

  describe('lazy promise creation', () => {
    it('should not allocate a promise on construction', () => {
      const controller = createController();
      expect(controller.stoppedPromise).toBeNull();
    });

    it('should not allocate a promise on start()', () => {
      const controller = createController();
      controller.start();
      expect(controller.stoppedPromise).toBeNull();
      expect(controller.stoppedResolve).toBeNull();
    });

    it('should return a resolved promise from waitUntilStopped() when already stopped', async () => {
      const controller = createController();
      // state is 'stopped' by default
      const promise = controller.waitUntilStopped();
      expect(promise).toBeInstanceOf(Promise);
      // Should not cache the promise since we are already stopped
      expect(controller.stoppedPromise).toBeNull();
      await expect(promise).resolves.toBeUndefined();
    });

    it('should lazily create a promise when waitUntilStopped() is called on a running animation', () => {
      const controller = createController();
      controller.start();
      expect(controller.stoppedPromise).toBeNull();

      const promise = controller.waitUntilStopped();
      expect(promise).toBeInstanceOf(Promise);
      expect(controller.stoppedPromise).toBe(promise);
      expect(controller.stoppedResolve).not.toBeNull();
    });

    it('should return the same promise on repeated waitUntilStopped() calls', () => {
      const controller = createController();
      controller.start();

      const promise1 = controller.waitUntilStopped();
      const promise2 = controller.waitUntilStopped();
      expect(promise1).toBe(promise2);
    });

    it('should reset the cached promise on a new start() cycle', () => {
      const controller = createController();
      controller.start();
      const promise1 = controller.waitUntilStopped();
      controller.stop();

      controller.start();
      expect(controller.stoppedPromise).toBeNull();

      const promise2 = controller.waitUntilStopped();
      expect(promise2).not.toBe(promise1);
    });
  });

  describe('start()', () => {
    it('should transition to scheduled state', () => {
      const controller = createController();
      controller.start();
      expect(controller.state).toBe('scheduled');
    });

    it('should register the animation with the manager', () => {
      const controller = createController();
      controller.start();
      expect(manager.registerAnimation).toHaveBeenCalledWith(animation);
    });

    it('should not re-register if already running', () => {
      const controller = createController();
      controller.start();

      // Simulate the animation emitting 'animating' to move to 'running'
      animation.emit('animating', {});
      expect(controller.state).toBe('running');

      controller.start();
      // Should only have been registered once
      expect(manager.registerAnimation).toHaveBeenCalledTimes(1);
    });
  });

  describe('stop()', () => {
    it('should resolve the promise when stopped manually', async () => {
      const controller = createController();
      controller.start();
      const promise = controller.waitUntilStopped();

      controller.stop();
      expect(controller.state).toBe('stopped');
      await expect(promise).resolves.toBeUndefined();
    });

    it('should emit stopped event when promise exists', () => {
      const controller = createController();
      const stoppedHandler = vi.fn();
      controller.on('stopped', stoppedHandler);

      controller.start();
      controller.waitUntilStopped(); // create the promise + resolve
      controller.stop();

      expect(stoppedHandler).toHaveBeenCalled();
    });

    it('should work without waitUntilStopped() being called (fire-and-forget)', () => {
      const controller = createController();
      const stoppedHandler = vi.fn();
      controller.on('stopped', stoppedHandler);
      controller.start();
      // No waitUntilStopped() call -- fire and forget
      expect(controller.stoppedResolve).toBeNull();

      // stop should not throw and should still emit 'stopped'
      controller.stop();
      expect(controller.state).toBe('stopped');
      expect(stoppedHandler).toHaveBeenCalledTimes(1);
    });

    it('should not emit stopped when already in stopped state', () => {
      const controller = createController();
      const stoppedHandler = vi.fn();
      controller.on('stopped', stoppedHandler);

      // Controller starts in 'stopped' state, calling stop() should be a no-op
      controller.stop();
      expect(stoppedHandler).not.toHaveBeenCalled();
    });

    it('should reset animation when stop is called with reset=true', () => {
      const controller = createController();
      controller.start();
      controller.stop(true);
      expect(animation.reset).toHaveBeenCalled();
    });
  });

  describe('pause()', () => {
    it('should transition to paused state', () => {
      const controller = createController();
      controller.start();
      controller.pause();
      expect(controller.state).toBe('paused');
    });

    it('should unregister the animation from the manager', () => {
      const controller = createController();
      controller.start();
      controller.pause();
      expect(manager.unregisterAnimation).toHaveBeenCalledWith(animation);
    });
  });

  describe('animation lifecycle events', () => {
    it('should transition to running when animation emits animating', () => {
      const controller = createController();
      controller.start();
      animation.emit('animating', {});
      expect(controller.state).toBe('running');
    });

    it('should resolve promise when animation finishes naturally', async () => {
      const controller = createController();
      controller.start();
      const promise = controller.waitUntilStopped();

      animation.emit('finished', {});
      expect(controller.state).toBe('stopped');
      await expect(promise).resolves.toBeUndefined();
    });

    it('should emit stopped event when animation finishes naturally', () => {
      const controller = createController();
      const stoppedHandler = vi.fn();
      controller.on('stopped', stoppedHandler);

      controller.start();
      animation.emit('finished', {});
      expect(stoppedHandler).toHaveBeenCalled();
    });

    it('should handle finish without waitUntilStopped (fire-and-forget)', () => {
      const controller = createController();
      controller.start();
      // No waitUntilStopped() call

      // Should not throw when the animation finishes
      animation.emit('finished', {});
      expect(controller.state).toBe('stopped');
      expect(controller.stoppedResolve).toBeNull();
    });

    it('should handle stopMethod reverse by re-registering finished listener', () => {
      const animationWithReverse = createMockAnimation({
        stopMethod: 'reverse' as const,
      });

      const controller = createController(manager, animationWithReverse);
      controller.start();
      animationWithReverse.emit('finished', {});

      // Should NOT be stopped -- reverse re-registers
      expect(controller.state).not.toBe('stopped');
      expect(animationWithReverse.reverse).toHaveBeenCalled();
    });

    it('should not resolve promise when looping', () => {
      const loopingAnimation = createMockAnimation({
        loop: true,
      });

      const controller = createController(manager, loopingAnimation);
      controller.start();
      // Call waitUntilStopped to trigger lazy promise creation
      controller.waitUntilStopped();

      loopingAnimation.emit('finished', {});

      // Should still be running, not stopped
      expect(controller.state).not.toBe('stopped');
      // Promise should still be pending (stoppedResolve not cleared)
      expect(controller.stoppedResolve).not.toBeNull();
    });

    it('should transition to stopped on destroy', () => {
      const controller = createController();
      controller.start();
      animation.emit('destroyed', {});
      expect(controller.state).toBe('stopped');
    });
  });
});
