/**
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  pathToDataURI,
  makeCreateComponent,
  fastForward
} from '@lightningjs/ui-components-test-utils';
import Tile from '.';
import Artwork from '../Artwork';
import { jest } from '@jest/globals';

const sampleImage = 'sampleImage';

const createComponent = makeCreateComponent(Tile);

describe('Tile', () => {
  let tile, testRenderer;

  beforeEach(async () => {
    [tile, testRenderer] = createComponent(
      { w: 300, h: 169, metadataLocation: 'standard' },
      {
        spyOnMethods: [
          '_update',
          '_updateArtwork',
          '_updateBadge',
          '_updateCheckbox',
          '_updateContent',
          '_updateMetadata',
          '_updateProgressBar',
          '_updateLabel'
        ]
      }
    );
    await tile.__updateSpyPromise;
    testRenderer.unfocus();
  });

  afterEach(() => {
    tile = null;
    testRenderer = null;
  });

  describe('basic', () => {
    it('renders', () => {
      const tree = testRenderer.toJSON(2);
      expect(tree).toMatchSnapshot();
    });

    it('should return the proper structure initially', () => {
      const templateObj = Tile._template();
      expect(templateObj.Tile).toMatchObject({
        Artwork: {
          type: Artwork,
          mount: 0.5
        }
      });
    });
    it('should have required properties', () => {
      expect(Tile.properties).toEqual(
        expect.arrayContaining([
          'artwork',
          'circle',
          'badge',
          'checkbox',
          'metadata',
          'metadataLocation',
          'persistentMetadata',
          'progressBar',
          'label'
        ])
      );
    });

    it('should have required tags', () => {
      expect(Tile.tags).toEqual(
        expect.arrayContaining([
          'Background',
          'Artwork',
          'Content',
          'Tile',
          { name: 'Badge', path: 'Content.Badge' },
          { name: 'Checkbox', path: 'Content.Checkbox' },
          { name: 'Metadata', path: 'Content.Metadata' },
          { name: 'ProgressBar', path: 'Content.ProgressBar' },
          { name: 'Label', path: 'Content.Label' }
        ])
      );
    });

    it('returns the proper scale', async () => {
      expect(tile.scale).toBe(1);
      testRenderer.focus();
      await tile.__updateSpyPromise;
      expect(tile.scale).toBe(1);
    });

    it('returns the proper value for default width of the foreground', async () => {
      tile.w = 300;
      tile.h = 180;
      await tile.__updateSpyPromise;
      expect(tile._foregroundDefaultWidth).toBe(tile.innerW * 0.75);
      tile.w = 300;
      tile.h = 169;
      await tile.__updateSpyPromise;
      expect(tile._foregroundDefaultWidth).toBe(tile.innerW * 0.5);
    });
  });

  describe('announce', () => {
    it('sets the announce string to the appropriate metadata announce strings', () => {
      const metadata = {
        title: 'Title',
        description: 'Description',
        logoTitle: 'Peacock'
      };
      const badge = { title: 'HD' };
      const label = { title: 'Live' };
      const progressBar = { progress: 0.5 };
      tile.patch({ metadata, badge, label, progressBar });
      tile.mode = 'focused';
      testRenderer.forceAllUpdates();
      expect(tile.announce).toEqual([
        [metadata.title, metadata.description, metadata.logoTitle],
        badge.title,
        label.title,
        `${progressBar.progress * 100}%`
      ]);
    });

    it('overrides the announce string', () => {
      const overrideString = 'Custom announce string';
      tile.announce = overrideString;
      testRenderer.forceAllUpdates();
      expect(tile.announce).toBe(overrideString);
    });
  });

  describe('artwork', () => {
    it('should accept src as getter at the root component level or nested inside artwork object', () => {
      expect(tile.src).toBeUndefined();

      const imageUrl1 = 'test.png';
      tile.src = imageUrl1;
      testRenderer.forceAllUpdates();
      expect(tile.src).toBe(imageUrl1);
      expect(tile.artwork && tile.artwork.src).toBeUndefined();
      expect(tile._Artwork.src).toBe(imageUrl1);

      const imageUrl2 = 'test.png';
      tile.artwork = { src: imageUrl2 };
      testRenderer.forceAllUpdates();
      expect(tile.src).toBe(imageUrl2);
      expect(tile.artwork.src).toBe(imageUrl2);
      expect(tile._Artwork.src).toBe(imageUrl2);

      tile.src = undefined;
      tile.artwork = { src: imageUrl2 };
      testRenderer.forceAllUpdates();
      expect(tile.src).toBe(imageUrl2);
      expect(tile.artwork.src).toBe(imageUrl2);
      expect(tile._Artwork.src).toBe(imageUrl2);
    });

    it('updates artwork in default state', async () => {
      tile._Artwork._Image.src = sampleImage;
      await tile.__updateImageSpyPromise;
      expect(tile._Artwork.constructor.name).toBe('Artwork');
      expect(tile._Artwork._Image.src).toBe('sampleImage');
      expect(tile._Artwork.mode).toBe('unfocused');
      expect(tile._Artwork.gradient).toBe(false);
      expect(tile._Artwork.w).toBe(tile._w);
      expect(tile._Artwork.h).toBe(tile._h);
      expect(tile._Artwork.x).toBe(tile._w / 2);
      expect(tile._Artwork.y).toBe(tile._h / 2);
      expect(tile._Artwork.style.radius).toBe(tile.style.radius);
    });

    it('updates artwork scale when imageScale is updated', async () => {
      tile.artwork = { src: sampleImage };
      testRenderer.focus();
      expect(tile._Artwork.style.imageScale).toBe(1); // imageScale for Artwork is 1
      tile.artwork.style = { imageScale: 2 };
      await tile.__updateArtworkSpyPromise;
      expect(tile._Artwork.style.imageScale).toBe(2);
    });

    it('should add foregroundImage if set', async () => {
      tile.artwork = {
        foregroundSrc: 'foo'
      };
      await tile.__updateArtworkSpyPromise;
      expect(tile._Artwork.foregroundSrc).toBe('foo');
    });
  });

  describe('badge', () => {
    it('should add badge if required and remove element when no longer needed', async () => {
      tile.badge = {
        title: 'test'
      };
      await tile.__updateBadgeSpyPromise;
      expect(tile._Badge).not.toBeUndefined();
      tile.itemLayout = { circle: true };
      await tile.__updateBadgeSpyPromise;
      expect(tile._Badge).toBeUndefined();
      tile.itemLayout = undefined;
      tile.badge = {
        title: 'changed'
      };
      await tile.__updateBadgeSpyPromise;
      expect(tile._Badge).not.toBeUndefined();

      tile.badge = {
        title: 'changed again'
      };
      await tile.__updateBadgeSpyPromise;
      expect(tile._Badge._transitions.title.targetValue).toBe('changed again');
      tile.shouldSmooth = false;
      tile.badge = {
        title: 'no smooth'
      };
      await tile.__updateBadgeSpyPromise;
      expect(tile._Badge.title).toBe('no smooth');
    });

    it('should not add badge if has a circle layout', async () => {
      tile.itemLayout = { circle: true };
      tile.badge = { title: 'test' };
      await tile.__updateBadgeSpyPromise;
      expect(tile._Badge).toBeUndefined();
    });
  });

  describe('circle layout', () => {
    it('should not add tag if has a circle layout', async () => {
      tile.itemLayout = { circle: true };
      tile.metadata = { tagTitle: 'test' };
      await tile.__updateTagSpyPromise;
      expect(tile._Tag).toBeUndefined();
    });

    it('returns the proper value for isCircleLayout', async () => {
      expect(tile._isCircleLayout).toBe(false);
      tile.itemLayout = {
        ratioX: 16,
        ratioY: 9,
        upCount: 5,
        circle: true
      };
      await tile.__updateSpyPromise;
      expect(tile._isCircleLayout).toBe(true);
    });
  });

  describe('label', () => {
    it('should add label if required and remove element when no longer needed', async () => {
      tile.label = {
        title: 'test'
      };
      await tile.__updateLabelSpyPromise;
      expect(tile._Label).not.toBeUndefined();
      tile.itemLayout = { circle: true };
      await tile.__updateTagSpyPromise;
      expect(tile._Tag).toBeUndefined();
      tile.itemLayout = undefined;
      tile.label = {
        title: 'changed'
      };
      await tile.__updateLabelSpyPromise;
      expect(tile._Label._transitions.title.targetValue).toBe('changed');
      tile.label = {
        title: 'changed again'
      };
      await tile.__updateLabelSpyPromise;
      expect(tile._Label._transitions.title.targetValue).toBe('changed again');
      tile.shouldSmooth = false;
      tile.label = {
        title: 'no smooth'
      };
      await tile.__updateLabelSpyPromise;
      expect(tile._Label.title).toBe('no smooth');
      tile.label = {
        title: 'with smooth'
      };
      await tile.__updateLabelSpyPromise;
      expect(tile._Label.title).toBe('with smooth');
    });
  });

  describe('Progress Bar', () => {
    it('should add a ProgressBar when progress is greater than 0 and remove it if no longer needed', async () => {
      expect(tile._ProgressBar).toBeUndefined();
      tile.progressBar = {
        progress: 0.9
      };
      await tile.__updateProgressBarSpyPromise;
      expect(tile._ProgressBar).not.toBeUndefined();
      tile.itemLayout = { w: 100, h: 200, circle: true };
      await tile.__updateProgressBarSpyPromise;
      fastForward([tile._ProgressBar]);
      testRenderer.update(); // Force redraw
      expect(tile._ProgressBar).toBeUndefined();
      tile.itemLayout = undefined;
      tile.shouldSmooth = false;
      tile.progressBar = {
        progress: 0.4
      };
      await tile.__updateProgressBarSpyPromise;
      expect(tile._ProgressBar).not.toBeUndefined();
      tile.progressBar = {
        progress: 0
      };
      await tile.__updateProgressBarSpyPromise;
      expect(tile._ProgressBar).toBeUndefined();
      tile.progressBar = {
        progress: 0.5
      };
      await tile.__updateProgressBarSpyPromise;
      expect(tile._ProgressBar).not.toBeUndefined();
      tile.shouldSmooth = true;
      tile.progressBar = {
        progress: 0
      };
      await tile.__updateProgressBarSpyPromise;
      fastForward([tile._ProgressBar]);
      testRenderer.update(); // Force redraw
      expect(tile._ProgressBar).toBeUndefined();
    });

    it('should adjust y position of metadata if inset and there is a progressbar', () => {
      tile.patch({
        progressBar: { progress: 0.5 },
        metadataLocation: 'inset',
        metadata: { title: 'test ' },
        mode: 'focused'
      });
      testRenderer.forceAllUpdates();
      expect(tile._metadataY).toBe(
        tile._ProgressBar.y - tile.style.paddingYBetweenContent
      );
    });

    it('should not patch progressBar if is in circle layout mode', async () => {
      tile.patch({
        progressBar: {
          progress: 0.5
        },
        itemLayout: {
          circle: true
        }
      });
      await tile.__updateProgressBarSpyPromise;
      expect(tile._ProgressBar).toBeUndefined();
    });
  });

  describe('checkbox', () => {
    it('should add a checkbox if required and remove the element when no longer needed', async () => {
      expect(tile._Checkbox).toBeUndefined();

      tile.checkbox = {
        checked: true
      };
      await tile.__updateCheckboxSpyPromise;
      expect(tile._Checkbox).not.toBeUndefined();

      tile.w = 100;
      await tile.__updateCheckboxSpyPromise;
      expect(tile._Checkbox._transitions.x.targetValue).toBe(70);

      tile.w = 200;
      tile.shouldSmooth = false;
      await tile.__updateCheckboxSpyPromise;
      expect(tile._Checkbox.x).toBe(170);

      tile.itemLayout = { circle: true };
      await tile.__updateCheckboxSpyPromise;
      expect(tile._Checkbox).toBeUndefined();

      tile.itemLayout = undefined;
      tile.requestUpdate(true);
      await tile.__updateCheckboxSpyPromise;
      expect(tile._Checkbox).not.toBeUndefined();

      tile.checkbox = {
        checked: false
      };
      await tile.__updateCheckboxSpyPromise;
      expect(tile._Checkbox).toBeUndefined();
    });
  });

  describe('logo', () => {
    const logo = pathToDataURI('src/assets/images/ic_lightning_white_32.png');
    it('should patch in a logo if provided', () => {
      tile.logo = logo;
      expect(tile._Icon).toBeUndefined();
    });
  });

  describe('metadata', () => {
    it('should update metadata and remove if no longer needed', async () => {
      expect(tile.metadata).toBeUndefined();

      tile.metadata = { title: 'test' };
      await tile.__updateMetadataSpyPromise;
      expect(tile.metadata).not.toBeUndefined();
      expect(tile._Metadata).not.toBeUndefined();

      tile.metadata = undefined;
      await tile.__updateMetadataSpyPromise;
      expect(tile._Metadata).toBeUndefined();
    });

    it('returns the proper value for gradient when has metadata is in focus and layout is inset', async () => {
      expect(tile._shouldShowGradient).toBe(false);
      // Overwrite _hasMetadata to always be true for test
      Object.defineProperty(tile, '_hasMetadata', {
        get() {
          return true;
        }
      });
      testRenderer.focus();
      await tile.__updateSpyPromise;
      expect(tile._shouldShowGradient).toBe(false);
      tile.progressBar = {
        progress: 0.5
      };
      await tile.__updateSpyPromise;
      expect(tile._shouldShowGradient).toBe(true);
      tile.progressBar = {
        progress: 0
      };
      tile.metadata = { title: 'test' };

      testRenderer.unfocus();
      await tile.__updateSpyPromise;

      expect(tile._shouldShowGradient).toBe(false);
      testRenderer.focus();
      await tile.__updateSpyPromise;
      expect(tile._shouldShowGradient).toBe(false);
      tile.metadataLocation = 'inset';
      tile.progressBar = {
        progress: 0.7
      };
      testRenderer.focus();
      await tile.__updateSpyPromise;
      expect(tile._shouldShowGradient).toBe(true);
    });
  });

  describe('marquee', () => {
    it('should reset the marquee animation when unfocused', async () => {
      const mockResetMarquee = jest.fn();
      jest.mock('../MetadataTile', () => {
        jest.fn().mockImplementationOnce(() => {
          return {
            ...jest.requireActual('../MetadataTile'),
            resetMarquee: mockResetMarquee
          };
        });
      });
      const [tile, testRenderer] = createComponent(
        {
          metadata: {
            title: 'long text'.repeat(30),
            description: 'long text'.repeat(30)
          }
        },
        {
          focused: false,
          spyOnMethods: ['_updateMetadata']
        }
      );

      await tile.__updateMetadataSpyPromise;

      expect(mockResetMarquee).not.toHaveBeenCalled();

      testRenderer.focus();
      await tile.__updateMetadataSpyPromise;

      expect(mockResetMarquee).not.toHaveBeenCalled();

      testRenderer.unfocus();

      expect(mockResetMarquee).not.toHaveBeenCalled();
    });
  });
});
