import { createComponentFactory, Spectator } from '@ngneat/spectator';
import { MonthYearBodyComponent } from './month-year-body.component';

import { DatepickerViewState } from '../../datepicker-view.state';
import { format, getMonth, getYear, startOfMonth } from 'date-fns';
import { RangeDatepicker } from '../../range-datepicker';

describe('MonthYearBodyComponent', () => {
  let spectator: Spectator<MonthYearBodyComponent>;
  const initialDisplayedDate = new Date(2024, 5, 15); // June 15, 2024
  const minDate = new Date(2024, 0, 1); // January 1, 2024
  const maxDate = new Date(2024, 11, 31); // December 31, 2024

  const datepickerMock = {
    min: jest.fn().mockReturnValue(minDate),
    max: jest.fn().mockReturnValue(maxDate),
  };

  const displayedDateSetSpy = jest.fn();
  // Create a function that returns the initial date.
  const displayedDateFunc = jest.fn(() => initialDisplayedDate);
  // Extend the function with a 'set' property using Object.assign.
  const displayedDateExtended = Object.assign(displayedDateFunc, {
    set: displayedDateSetSpy,
  });

  const viewStateMock = {
    displayedDate: displayedDateExtended,
    displayedView: jest.fn(() => 'expectedValue'), // Mock function returning the expected value.
  };

  const createComponent = createComponentFactory({
    component: MonthYearBodyComponent,
    providers: [
      { provide: RangeDatepicker, useValue: datepickerMock },
      { provide: DatepickerViewState, useValue: viewStateMock },
    ],
  });

  beforeEach(() => {
    spectator = createComponent();
  });

  it('should create the component', () => {
    expect(spectator.component).toBeTruthy();
  });

  it('should compute the current displayed month and year from viewState', () => {
    const comp = spectator.component;
    expect(comp.currentDisplayedMonth).toBe(getMonth(initialDisplayedDate));
    expect(comp.currentDisplayedYear).toBe(getYear(initialDisplayedDate));
  });

  it('should compute months array filtering out dates outside min and max bounds', () => {
    const comp = spectator.component;
    const months = comp.months();
    expect(months).toHaveLength(12);
    months.forEach((month, index) => {
      // Create a date for comparison.
      const date = new Date(comp.currentDisplayedYear, index, 1);
      const beforeMin = minDate && date < startOfMonth(minDate);
      const afterMax = maxDate && date > startOfMonth(maxDate);

      if (beforeMin || afterMax) {
        expect(month).toBeNull();
      } else {
        expect(month).toEqual({
          label: format(date, 'LLLL'),
          value: date,
        });
      }
    });
  });

  it('should compute years array based on min and max bounds from Datepicker', () => {
    const comp = spectator.component;
    const yearsArray = comp.years();
    const expectedMinYear = getYear(minDate);
    const expectedMaxYear = getYear(maxDate);
    const expectedLength = expectedMaxYear - expectedMinYear + 1;
    expect(yearsArray).toHaveLength(expectedLength);
    // Ensure years are ordered descending from maxYear to minYear.
    yearsArray.forEach((yearObj, idx) => {
      const expectedYear = expectedMaxYear - idx;
      expect(yearObj.label).toBe(expectedYear.toString());
      expect(getYear(yearObj.value)).toBe(expectedYear);
    });
  });

  it('should compute canSave as true when current displayed month/year match viewState date', () => {
    const comp = spectator.component;
    // Because currentDisplayedMonth/year are derived from viewState.displayedDate,
    // they match, so canSave should be true.
    expect(comp.canSave()).toBe(true);
  });

  it('should update month while keeping year intact when updateMonth is called', () => {
    const comp = spectator.component;
    const newDate = new Date(2024, 8, 1); // September 1, 2024
    comp.updateMonth(newDate);
    // Year should remain as in initialDisplayedDate
    const expectedDate = new Date(
      getYear(initialDisplayedDate),
      getMonth(newDate),
    );
    expect(displayedDateSetSpy).toHaveBeenCalledWith(expectedDate);
  });

  it('should update year while keeping month intact when updateYear is called', () => {
    const comp = spectator.component;
    const newDate = new Date(2026, 0, 1); // Year 2026, month value ignored
    comp.updateYear(newDate);
    // Month should remain as in initialDisplayedDate
    const expectedDate = new Date(
      getYear(newDate),
      getMonth(initialDisplayedDate),
    );
    expect(displayedDateSetSpy).toHaveBeenCalledWith(expectedDate);
  });

  it('should rollback displayed month and year to original values', () => {
    const comp = spectator.component;
    comp.rollbackDisplayedMonthYear();
    const expectedDate = new Date(
      comp.currentDisplayedYear,
      comp.currentDisplayedMonth,
    );
    expect(displayedDateSetSpy).toHaveBeenCalledWith(expectedDate);
  });

  it('should correctly identify the selected month', () => {
    const comp = spectator.component;
    const testDateSameMonth = new Date(
      getYear(initialDisplayedDate),
      getMonth(initialDisplayedDate),
      5,
    );
    const testDateDifferentMonth = new Date(
      getYear(initialDisplayedDate),
      getMonth(initialDisplayedDate) + 1,
      5,
    );
    expect(comp.isSelectedMonth(testDateSameMonth)).toBe(true);
    expect(comp.isSelectedMonth(testDateDifferentMonth)).toBe(false);
  });

  it('should correctly identify the selected year', () => {
    const comp = spectator.component;
    const testDateSameYear = new Date(getYear(initialDisplayedDate), 0, 1);
    const testDateDifferentYear = new Date(
      getYear(initialDisplayedDate) + 1,
      0,
      1,
    );
    expect(comp.isSelectedYear(testDateSameYear)).toBe(true);
    expect(comp.isSelectedYear(testDateDifferentYear)).toBe(false);
  });
});
