import { Directive } from '@angular/core';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DatePipe } from '@angular/common';
import { NgIconComponent } from '@ng-icons/core';
import { CalendarBodyComponent } from './calendar-body.component';
import { DatepickerViewState } from '../../datepicker-view.state';
import {
  eachDayOfInterval,
  endOfMonth,
  endOfWeek,
  startOfMonth,
  startOfWeek,
} from 'date-fns';
import { RangeDatepicker } from '../../range-datepicker';

// Stub directive override to prevent evaluation of the real computed property
@Directive({
  selector: '[uiCalendarBodyCell]',
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
class CalendarBodyCellDirectiveStub {
  // Provide a default getter to avoid errors in host bindings.
  get isDateInRange() {
    return false;
  }
}

describe('CalendarBodyComponent', () => {
  let spectator: Spectator<CalendarBodyComponent>;
  const initialDisplayedDate = new Date(2024, 5, 15); // June 15, 2024

  const datepickerMock = {
    value: jest.fn(),
    min: jest.fn(),
    max: jest.fn(),
    range: jest.fn(), // used only in the directives; we do not test these cases here
  };

  const displayedDateSetSpy = jest.fn();
  const displayedDateFn = jest.fn(() => initialDisplayedDate);
  const displayedDateSignal = Object.assign(displayedDateFn, {
    set: displayedDateSetSpy,
  });

  const viewStateMock = {
    displayedDate: displayedDateSignal,
  };

  const createComponent = createComponentFactory({
    component: CalendarBodyComponent,
    schemas: [NO_ERRORS_SCHEMA],
    // Use stub to prevent executing the original CalendarBodyCellDirective computed code.
    imports: [DatePipe, NgIconComponent, CalendarBodyCellDirectiveStub],
    providers: [
      { provide: RangeDatepicker, useValue: datepickerMock },
      { provide: DatepickerViewState, useValue: viewStateMock },
    ],
  });

  beforeEach(() => {
    datepickerMock.min.mockReturnValue(undefined);
    datepickerMock.max.mockReturnValue(undefined);
    datepickerMock.range.mockReturnValue(undefined);
    // Default: return null to avoid unintended destructuring
    datepickerMock.value.mockReturnValue(null);
    jest.clearAllMocks();
    spectator = createComponent();
  });

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

  // Additional tests covering robust scenarios
  describe('selectedDates computed property', () => {
    it('should return an array with a single valid date when a singular date is provided', () => {
      const validDate = new Date(2024, 5, 20);
      datepickerMock.value.mockReturnValue([validDate]);
      spectator = createComponent();
      expect(spectator.component.selectedDates()).toEqual([validDate]);
    });

    it('should filter out invalid dates when an array is provided', () => {
      const validDate1 = new Date(2024, 5, 20);
      const validDate2 = new Date(2024, 5, 25);
      const invalidDate = 'invalid date' as unknown as Date;
      datepickerMock.value.mockReturnValue([
        validDate1,
        invalidDate,
        validDate2,
      ]);
      spectator = createComponent();
      expect(spectator.component.selectedDates()).toEqual([
        validDate1,
        validDate2,
      ]);
    });

    it('should return an empty array when no date is provided', () => {
      datepickerMock.value.mockReturnValue(null);
      spectator = createComponent();
      expect(spectator.component.selectedDates()).toEqual([]);
    });

    it('should return an array of valid dates when all dates in array are valid', () => {
      const validDate1 = new Date(2024, 5, 10);
      const validDate2 = new Date(2024, 5, 15);
      datepickerMock.value.mockReturnValue([validDate1, validDate2]);
      spectator = createComponent();
      expect(spectator.component.selectedDates()).toEqual([
        validDate1,
        validDate2,
      ]);
    });
  });

  describe('daysOfWeek computed property', () => {
    it('should return an array of exactly seven abbreviated day names', () => {
      const result = spectator.component.daysOfWeek();
      expect(result).toHaveLength(7);
      result.forEach((letter) => {
        expect(letter).toMatch(/^[A-Z]$/);
      });
    });
  });

  describe('calendarDays computed signal', () => {
    it('should compute the correct interval of days for the displayed month', () => {
      const { calendarDays } = spectator.component;
      const start = startOfWeek(startOfMonth(initialDisplayedDate), {
        weekStartsOn: 1,
      });
      const end = endOfWeek(endOfMonth(initialDisplayedDate), {
        weekStartsOn: 1,
      });
      const expectedDays = eachDayOfInterval({ start, end });
      expect(calendarDays()).toEqual(expectedDays);
    });
  });

  describe('hasValue computed property', () => {
    it('should return true for a valid singular date', () => {
      const validDate = new Date(2024, 5, 20);
      datepickerMock.value.mockReturnValue([validDate]);
      spectator = createComponent();
      expect(spectator.component.hasValue()).toBe(true);
    });

    it('should return true if at least one valid date exists in an array', () => {
      const validDate = new Date(2024, 5, 20);
      datepickerMock.value.mockReturnValue([null, validDate]);
      spectator = createComponent();
      expect(spectator.component.hasValue()).toBe(true);
    });

    it('should return false when no valid date exists', () => {
      datepickerMock.value.mockReturnValue(undefined);
      spectator = createComponent();
      expect(spectator.component.hasValue()).toBe(false);
    });
  });

  describe('previous() method', () => {
    it('should update displayedDate to the previous month', () => {
      spectator.component.previous();
      const currentDate = displayedDateFn();
      const expectedDate = new Date(
        currentDate.getFullYear(),
        currentDate.getMonth() - 1,
      );
      expect(displayedDateSetSpy).toHaveBeenCalledWith(expectedDate);
    });
  });

  describe('next() method', () => {
    it('should update displayedDate to the next month', () => {
      spectator.component.next();
      const currentDate = displayedDateFn();
      const expectedDate = new Date(
        currentDate.getFullYear(),
        currentDate.getMonth() + 1,
      );
      expect(displayedDateSetSpy).toHaveBeenCalledWith(expectedDate);
    });
  });

  // Robustness tests for computed properties under unexpected inputs
  describe('robustness of computed properties', () => {
    it('should handle when datepicker value is an empty array', () => {
      datepickerMock.value.mockReturnValue([]);
      spectator = createComponent();
      expect(spectator.component.selectedDates()).toEqual([]);
      expect(spectator.component.hasValue()).toBe(false);
    });

    it('should handle non-date values gracefully in selectedDates', () => {
      datepickerMock.value.mockReturnValue(['foo', 'bar'] as unknown as Date);
      spectator = createComponent();
      expect(spectator.component.selectedDates()).toEqual([]);
      expect(spectator.component.hasValue()).toBe(false);
    });
  });

  // Additional tests for edge cases
  describe('edge case tests', () => {
    it('should not throw when datepicker.value returns a valid array structure for the directive', () => {
      // For the directive computed property (stubbed), we set value as an array of two valid dates.
      const startDate = new Date(2024, 5, 1);
      const endDate = new Date(2024, 5, 30);
      datepickerMock.value.mockReturnValue([startDate, endDate]);
      spectator = createComponent();
      // These tests focus on component computed properties, the stub prevents errors in the directive.
      expect(() => spectator.detectChanges()).not.toThrow();
    });
    it('should not throw when datepicker.value returns a singular date for component computed properties', () => {
      const validDate = new Date(2024, 5, 20);
      datepickerMock.value.mockReturnValue([validDate]);
      spectator = createComponent();
      expect(() => spectator.detectChanges()).not.toThrow();
    });
  });
});
