import dayjs from '../../src/libs/dayjs';
import {
  activateAfterFirstPaymentSchedules,
  calculateFirstGrantAt,
  calculateGrantAmount,
  calculateNextGrantAt,
  calculateScheduleSeq,
  calculateScheduledGrantExpiresAt,
  getCreditConfigWithSchedule,
  getIntervalMs,
  getScheduleJobId,
  handleScheduledCredit,
  hasActiveCreditSchedule,
  initializeCreditSchedule,
  resetPeriodGrantCounter,
  stopCreditSchedule,
} from '../../src/libs/credit-schedule';
import { calculateExpiresAt } from '../../src/libs/credit-grant';
import { getLock } from '../../src/libs/lock';
import { getSubscriptionItemPrice } from '../../src/libs/subscription';
import { CreditGrant, PaymentCurrency, Price, Subscription, SubscriptionItem } from '../../src/store/models';

jest.mock('../../src/libs/logger', () => ({
  __esModule: true,
  default: {
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
  },
}));

jest.mock('../../src/libs/event', () => ({
  events: {
    emit: jest.fn(),
  },
}));

jest.mock('../../src/libs/lock', () => ({
  getLock: jest.fn(),
}));

jest.mock('../../src/libs/credit-grant', () => ({
  createCreditGrant: jest.fn(),
  calculateExpiresAt: jest.fn(),
}));

jest.mock('../../src/libs/subscription', () => ({
  getSubscriptionItemPrice: jest.fn(),
}));


jest.mock('../../src/store/models', () => ({
  CreditGrant: {
    findOne: jest.fn(),
    findByPk: jest.fn(),
  },
  PaymentCurrency: {
    findByPk: jest.fn(),
  },
  Price: {
    findAll: jest.fn(),
    findByPk: jest.fn(),
    expand: jest.fn(),
  },
  Product: {},
  Subscription: {
    findByPk: jest.fn(),
    findAll: jest.fn(),
  },
  SubscriptionItem: {
    findAll: jest.fn(),
  },
}));

describe('libs/credit-schedule.ts', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    jest.restoreAllMocks();
  });

  describe('basic helpers', () => {
    it('builds schedule job id with seq', () => {
      expect(getScheduleJobId('sub_1', 'price_1', 3)).toBe('schedule-sub_1-price_1-3');
    });

    it('calculates schedule seq for non-month intervals', () => {
      const anchorAt = 1000;
      const intervalValue = 1;
      expect(calculateScheduleSeq(anchorAt, anchorAt - 1, intervalValue, 'hour')).toBe(0);
      expect(calculateScheduleSeq(anchorAt, anchorAt, intervalValue, 'hour')).toBe(1);
      expect(calculateScheduleSeq(anchorAt, anchorAt + 3600, intervalValue, 'hour')).toBe(2);
    });

    it('calculates schedule seq for month intervals', () => {
      const anchorAt = 1704067200; // 2024-01-01T00:00:00Z
      const firstMonth = dayjs.unix(anchorAt).add(1, 'month').unix();
      expect(calculateScheduleSeq(anchorAt, anchorAt, 1, 'month')).toBe(1);
      expect(calculateScheduleSeq(anchorAt, firstMonth, 1, 'month')).toBe(2);
    });

    it('calculates schedule seq around leap day month ends', () => {
      const jan31 = dayjs.utc('2024-01-31T00:00:00Z').unix();
      const feb29 = dayjs.utc('2024-02-29T00:00:00Z').unix();
      const mar29 = dayjs.utc('2024-03-29T00:00:00Z').unix();

      expect(calculateScheduleSeq(jan31, jan31, 1, 'month')).toBe(1);
      expect(calculateScheduleSeq(jan31, feb29, 1, 'month')).toBe(2);
      expect(calculateScheduleSeq(feb29, mar29, 1, 'month')).toBe(2);
    });

    it('returns interval ms for day and month', () => {
      expect(getIntervalMs(2, 'day')).toBe(2 * 24 * 60 * 60 * 1000);
      expect(getIntervalMs(1, 'week')).toBe(7 * 24 * 60 * 60 * 1000);
      const anchorAt = 1704067200;
      const ref = dayjs.unix(anchorAt);
      expect(getIntervalMs(1, 'month', anchorAt)).toBe(ref.add(1, 'month').diff(ref));
    });

    it('calculates next grant time', () => {
      const anchorAt = 1000;
      expect(calculateNextGrantAt(anchorAt, 2, 1, 'hour')).toBe(8200);

      const monthAnchor = 1704067200;
      const expected = dayjs.unix(monthAnchor).add(2, 'month').unix();
      expect(calculateNextGrantAt(monthAnchor, 2, 1, 'month')).toBe(expected);
    });
  });

  describe('config and amount helpers', () => {
    it('extracts credit config with schedule', () => {
      const price = {
        metadata: {
          credit_config: {
            schedule: {
              enabled: true,
              delivery_mode: 'schedule',
            },
          },
        },
      };

      expect(getCreditConfigWithSchedule(price)).toEqual({
        creditConfig: price.metadata.credit_config,
        scheduleConfig: price.metadata.credit_config.schedule,
      });

      const disabled = { metadata: { credit_config: { schedule: { enabled: false } } } };
      expect(getCreditConfigWithSchedule(disabled)).toBeNull();
    });

    it('calculates first grant time based on timing', () => {
      jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00Z'));
      const now = dayjs().unix();
      const subscription = {
        start_date: now + 600,
        trial_end: now + 1200,
      } as any;

      expect(calculateFirstGrantAt(subscription, { first_grant_timing: 'immediate' } as any)).toBe(
        subscription.start_date
      );
      expect(calculateFirstGrantAt(subscription, { first_grant_timing: 'after_trial' } as any)).toBe(
        subscription.trial_end
      );
      expect(calculateFirstGrantAt(subscription, { first_grant_timing: 'after_first_payment' } as any)).toBeNull();
      jest.useRealTimers();
    });

    it('calculates grant amount using amount_per_grant or credit_amount', () => {
      const currency = { decimal: 2 } as any;
      const creditConfig = { credit_amount: '1000' } as any;

      expect(
        calculateGrantAmount(creditConfig, { amount_per_grant: '200' } as any, currency)
      ).toBe('20000');
      expect(calculateGrantAmount(creditConfig, {} as any, currency)).toBe('100000');
    });

    it('calculates scheduled grant expiration', () => {
      const expiresAt = calculateExpiresAt as jest.Mock;
      expiresAt.mockReturnValue(999);

      const creditConfig = { valid_duration_value: 3, valid_duration_unit: 'day' } as any;
      const scheduleConfig = { expire_with_next_grant: false } as any;

      expect(calculateScheduledGrantExpiresAt(creditConfig, scheduleConfig, 1234)).toBe(999);

      expect(
        calculateScheduledGrantExpiresAt(
          creditConfig,
          { expire_with_next_grant: true } as any,
          5678
        )
      ).toBe(5678);
    });
  });

  describe('initializeCreditSchedule', () => {
    it('initializes schedule state for credit prices', async () => {
      jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00Z'));
      const now = dayjs().unix();

      const subscription = {
        id: 'sub_1',
        status: 'active',
        start_date: now - 60,
        trial_end: null,
        credit_schedule_state: undefined,
        isActive: jest.fn().mockReturnValue(true),
        update: jest.fn().mockResolvedValue(undefined),
      } as any;

      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);

      const items = [{ price_id: 'price_1' }, { price_id: 'price_2' }];
      (SubscriptionItem.findAll as jest.Mock).mockResolvedValue(items);

      const prices = [
        {
          id: 'price_1',
          product: { type: 'credit' },
          metadata: {
            credit_config: {
              credit_amount: '1000',
              currency_id: 'usd',
              schedule: {
                enabled: true,
                delivery_mode: 'schedule',
                interval_value: 1,
                interval_unit: 'day',
                first_grant_timing: 'immediate',
              },
            },
          },
        },
        {
          id: 'price_2',
          product: { type: 'subscription' },
        },
      ];
      (Price.findAll as jest.Mock).mockResolvedValue(prices);

      const state = await initializeCreditSchedule(subscription);

      expect(state).toEqual(
        expect.objectContaining({
          price_1: expect.objectContaining({
            enabled: true,
            schedule_anchor_at: now,
            next_grant_at: now,
            last_grant_seq: 0,
            grants_in_current_period: 0,
          }),
        })
      );
      expect(subscription.update).toHaveBeenCalled();
      expect(lock.release).toHaveBeenCalled();
      jest.useRealTimers();
    });

    it('returns null when subscription is not active', async () => {
      const subscription = {
        id: 'sub_1',
        status: 'canceled',
        isActive: jest.fn().mockReturnValue(false),
      } as any;

      const state = await initializeCreditSchedule(subscription);

      expect(state).toBeNull();
      expect(getLock).not.toHaveBeenCalled();
    });

    it('returns null when no subscription items found', async () => {
      const subscription = {
        id: 'sub_1',
        status: 'active',
        credit_schedule_state: undefined,
        isActive: jest.fn().mockReturnValue(true),
        update: jest.fn().mockResolvedValue(undefined),
      } as any;

      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      (SubscriptionItem.findAll as jest.Mock).mockResolvedValue([]);

      const state = await initializeCreditSchedule(subscription);

      expect(state).toBeNull();
      expect(subscription.update).not.toHaveBeenCalled();
      expect(lock.release).toHaveBeenCalled();
    });
  });

  describe('activateAfterFirstPaymentSchedules', () => {
    it('activates after_first_payment schedules and returns jobs', async () => {
      const subscription = {
        id: 'sub_1',
        status: 'active',
        isActive: jest.fn().mockReturnValue(true),
        credit_schedule_state: {},
        update: jest.fn().mockResolvedValue(undefined),
      } as any;

      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);

      const items = [{ toJSON: () => ({ id: 'item_1' }) }];
      (SubscriptionItem.findAll as jest.Mock).mockResolvedValue(items);
      (Price.expand as jest.Mock).mockResolvedValue([{ id: 'expanded_1' }]);

      (getSubscriptionItemPrice as jest.Mock).mockReturnValue({
        id: 'price_1',
        product: { type: 'credit' },
        metadata: {
          credit_config: {
            schedule: {
              enabled: true,
              delivery_mode: 'schedule',
              first_grant_timing: 'after_first_payment',
              interval_value: 1,
              interval_unit: 'day',
            },
          },
        },
      });

      const jobs = await activateAfterFirstPaymentSchedules(subscription, 1234);

      expect(jobs).toHaveLength(1);
      expect(jobs[0]!.jobId).toBe('schedule-sub_1-price_1-1');
      expect(jobs[0]!.runAt).toBe(1234);
      expect(subscription.update).toHaveBeenCalled();
      expect(lock.release).toHaveBeenCalled();
    });

    it('returns empty when subscription is not active', async () => {
      const subscription = {
        id: 'sub_1',
        status: 'canceled',
        isActive: jest.fn().mockReturnValue(false),
      } as any;

      const jobs = await activateAfterFirstPaymentSchedules(subscription, 1234);

      expect(jobs).toEqual([]);
      expect(getLock).not.toHaveBeenCalled();
    });
  });

  describe('handleScheduledCredit (idempotent path)', () => {
    it('updates state and schedules next job when grant already exists', async () => {
      jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00Z'));
      const now = dayjs().unix();
      const subscription = {
        id: 'sub_1',
        customer_id: 'cus_1',
        status: 'active',
        isActive: jest.fn().mockReturnValue(true),
        credit_schedule_state: {
          price_1: {
            enabled: true,
            schedule_anchor_at: now,
            next_grant_at: now,
            last_grant_seq: 0,
            grants_in_current_period: 0,
          },
        },
        update: jest.fn().mockResolvedValue(undefined),
      } as any;

      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      (Subscription.findByPk as jest.Mock).mockResolvedValue(subscription);

      (Price.findByPk as jest.Mock).mockResolvedValue({
        id: 'price_1',
        product: { type: 'credit' },
        metadata: {
          credit_config: {
            credit_amount: '1000',
            currency_id: 'usd',
            schedule: {
              enabled: true,
              delivery_mode: 'schedule',
              interval_value: 1,
              interval_unit: 'hour',
            },
          },
        },
      });

      (CreditGrant.findOne as jest.Mock).mockResolvedValue({ id: 'cg_1' });

      const result = await handleScheduledCredit({
        subscriptionId: 'sub_1',
        priceId: 'price_1',
        scheduledAt: now,
        action: 'create_from_schedule',
      });

      expect(result?.jobId).toBe('schedule-sub_1-price_1-2');
      expect(result?.runAt).toBe(now + 3600);
      expect(subscription.update).toHaveBeenCalled();
      expect(lock.release).toHaveBeenCalled();
      jest.useRealTimers();
    });
  });

  describe('handleScheduledCredit (defensive paths)', () => {
    const job = {
      subscriptionId: 'sub_1',
      priceId: 'price_1',
      scheduledAt: 1000,
      action: 'create_from_schedule' as const,
    };

    it('returns null when subscription not found', async () => {
      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      (Subscription.findByPk as jest.Mock).mockResolvedValue(null);

      const result = await handleScheduledCredit(job);

      expect(result).toBeNull();
      expect(lock.release).toHaveBeenCalled();
    });

    it('returns null when subscription not active', async () => {
      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      (Subscription.findByPk as jest.Mock).mockResolvedValue({
        id: 'sub_1',
        status: 'canceled',
        isActive: jest.fn().mockReturnValue(false),
      } as any);

      const result = await handleScheduledCredit(job);

      expect(result).toBeNull();
      expect(lock.release).toHaveBeenCalled();
    });

    it('returns null when schedule not enabled for price', async () => {
      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      (Subscription.findByPk as jest.Mock).mockResolvedValue({
        id: 'sub_1',
        status: 'active',
        isActive: jest.fn().mockReturnValue(true),
        credit_schedule_state: {
          price_1: { enabled: false },
        },
      } as any);

      const result = await handleScheduledCredit(job);

      expect(result).toBeNull();
      expect(Price.findByPk).not.toHaveBeenCalled();
      expect(lock.release).toHaveBeenCalled();
    });

    it('returns null when price not found', async () => {
      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      (Subscription.findByPk as jest.Mock).mockResolvedValue({
        id: 'sub_1',
        status: 'active',
        isActive: jest.fn().mockReturnValue(true),
        credit_schedule_state: {
          price_1: { enabled: true },
        },
      } as any);
      (Price.findByPk as jest.Mock).mockResolvedValue(null);

      const result = await handleScheduledCredit(job);

      expect(result).toBeNull();
      expect(lock.release).toHaveBeenCalled();
    });

    it('returns null when price has no schedule config', async () => {
      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      (Subscription.findByPk as jest.Mock).mockResolvedValue({
        id: 'sub_1',
        status: 'active',
        isActive: jest.fn().mockReturnValue(true),
        credit_schedule_state: {
          price_1: { enabled: true },
        },
      } as any);
      (Price.findByPk as jest.Mock).mockResolvedValue({
        id: 'price_1',
        product: { type: 'credit' },
        metadata: {},
      });

      const result = await handleScheduledCredit(job);

      expect(result).toBeNull();
      expect(lock.release).toHaveBeenCalled();
    });

    it('returns null when currency not found', async () => {
      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);
      const subscription = {
        id: 'sub_1',
        customer_id: 'cus_1',
        status: 'active',
        isActive: jest.fn().mockReturnValue(true),
        credit_schedule_state: {
          price_1: {
            enabled: true,
            schedule_anchor_at: 1000,
            next_grant_at: 1000,
            last_grant_seq: 0,
            grants_in_current_period: 0,
          },
        },
        update: jest.fn().mockResolvedValue(undefined),
      } as any;
      (Subscription.findByPk as jest.Mock).mockResolvedValue(subscription);
      (Price.findByPk as jest.Mock).mockResolvedValue({
        id: 'price_1',
        product: { type: 'credit' },
        metadata: {
          credit_config: {
            credit_amount: '1000',
            currency_id: 'usd',
            schedule: {
              enabled: true,
              delivery_mode: 'schedule',
              interval_value: 1,
              interval_unit: 'day',
            },
          },
        },
      });
      (CreditGrant.findOne as jest.Mock).mockResolvedValue(null);
      (PaymentCurrency.findByPk as jest.Mock).mockResolvedValue(null);

      const result = await handleScheduledCredit(job);

      expect(result).toBeNull();
      expect(subscription.update).toHaveBeenCalledWith({
        credit_schedule_state: expect.objectContaining({
          price_1: expect.objectContaining({
            last_error: 'Currency not found: usd',
          }),
        }),
      });
      expect(lock.release).toHaveBeenCalled();
    });
  });

  describe('handleScheduledCredit (max grants per period)', () => {
    it('recalculates nextGrantAt when max_grants_per_period reached', async () => {
      jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00Z'));
      const now = dayjs().unix();
      const lock = { acquire: jest.fn().mockResolvedValue(undefined), release: jest.fn() };
      (getLock as jest.Mock).mockReturnValue(lock);

      const subscription = {
        id: 'sub_1',
        customer_id: 'cus_1',
        status: 'active',
        current_period_end: now - 10,
        isActive: jest.fn().mockReturnValue(true),
        credit_schedule_state: {
          price_1: {
            enabled: true,
            schedule_anchor_at: now - 1000,
            next_grant_at: now - 100,
            last_grant_seq: 1,
            grants_in_current_period: 2,
          },
        },
        update: jest.fn().mockResolvedValue(undefined),
      } as any;
      (Subscription.findByPk as jest.Mock).mockResolvedValue(subscription);

      (Price.findByPk as jest.Mock).mockResolvedValue({
        id: 'price_1',
        product: { type: 'credit' },
        metadata: {
          credit_config: {
            credit_amount: '1000',
            currency_id: 'usd',
            schedule: {
              enabled: true,
              delivery_mode: 'schedule',
              interval_value: 1,
              interval_unit: 'week',
              max_grants_per_period: 2,
            },
          },
        },
      });
      (CreditGrant.findOne as jest.Mock).mockResolvedValue(null);

      const result = await handleScheduledCredit({
        subscriptionId: 'sub_1',
        priceId: 'price_1',
        scheduledAt: now,
        action: 'create_from_schedule',
      });

      const expectedNextGrantAt = now + 7 * 24 * 60 * 60;
      expect(result?.jobId).toBe('schedule-sub_1-price_1-2');
      expect(result?.runAt).toBe(expectedNextGrantAt);
      expect(subscription.update).toHaveBeenCalledWith({
        credit_schedule_state: expect.objectContaining({
          price_1: expect.objectContaining({
            next_grant_at: expectedNextGrantAt,
            last_error: undefined,
          }),
        }),
      });
      expect(lock.release).toHaveBeenCalled();
      jest.useRealTimers();
    });
  });

  describe('stopCreditSchedule & helpers', () => {
    it('stops schedule and returns pending job ids', async () => {
      const subscription = {
        id: 'sub_1',
        credit_schedule_state: {
          price_1: { enabled: true, last_grant_seq: 2, next_grant_at: 100 },
          price_2: { enabled: true, last_grant_seq: 0, next_grant_at: 200 },
        },
        update: jest.fn().mockResolvedValue(undefined),
      } as any;

      const jobIds = await stopCreditSchedule(subscription);

      expect(jobIds).toEqual(['schedule-sub_1-price_1-3', 'schedule-sub_1-price_2-1']);
      expect(subscription.update).toHaveBeenCalledWith({ credit_schedule_state: undefined });
    });

    it('checks for active schedule entries', () => {
      const subscription = {
        credit_schedule_state: {
          price_1: { enabled: true, next_grant_at: 100 },
          price_2: { enabled: false, next_grant_at: 200 },
        },
      } as any;

      expect(hasActiveCreditSchedule(subscription)).toBe(true);
      expect(hasActiveCreditSchedule({ credit_schedule_state: undefined } as any)).toBe(false);
    });

    it('resets period grant counters', async () => {
      const subscription = {
        id: 'sub_1',
        credit_schedule_state: {
          price_1: { enabled: true, grants_in_current_period: 3 },
          price_2: { enabled: true, grants_in_current_period: 1 },
        },
        update: jest.fn().mockResolvedValue(undefined),
      } as any;

      await resetPeriodGrantCounter(subscription);
      expect(subscription.update).toHaveBeenCalledWith({
        credit_schedule_state: {
          price_1: { enabled: true, grants_in_current_period: 0 },
          price_2: { enabled: true, grants_in_current_period: 0 },
        },
      });
    });
  });
});
