import { describe, it, expect } from 'vitest';
import {
	monthBoundaryFrom,
	monthBoundaryTo,
	buildMonthSequence,
	fillDensityGaps,
	aggregateToYears,
	selectionBoundaryFrom,
	selectionBoundaryTo,
	clipBucketsToRange,
	tickIndicesFor,
	formatTickLabel
} from './monthBuckets';

describe('monthBoundaryFrom', () => {
	it('returns the first day of the given month', () => {
		expect(monthBoundaryFrom('1915-08')).toBe('1915-08-01');
	});

	it('handles January', () => {
		expect(monthBoundaryFrom('1920-01')).toBe('1920-01-01');
	});
});

describe('monthBoundaryTo', () => {
	it('returns the last day of a 31-day month', () => {
		expect(monthBoundaryTo('1915-08')).toBe('1915-08-31');
	});

	it('returns the last day of a 30-day month', () => {
		expect(monthBoundaryTo('1915-04')).toBe('1915-04-30');
	});

	it('returns 28 for February in a non-leap year', () => {
		expect(monthBoundaryTo('1915-02')).toBe('1915-02-28');
	});

	it('returns 29 for February in a leap year', () => {
		expect(monthBoundaryTo('1916-02')).toBe('1916-02-29');
	});
});

describe('buildMonthSequence', () => {
	it('returns a single month when min and max are in the same month', () => {
		expect(buildMonthSequence('1915-08-03', '1915-08-22')).toEqual(['1915-08']);
	});

	it('returns months from minDate through maxDate inclusive', () => {
		expect(buildMonthSequence('1915-08-03', '1915-11-15')).toEqual([
			'1915-08',
			'1915-09',
			'1915-10',
			'1915-11'
		]);
	});

	it('crosses year boundaries correctly', () => {
		expect(buildMonthSequence('1915-11-30', '1916-02-01')).toEqual([
			'1915-11',
			'1915-12',
			'1916-01',
			'1916-02'
		]);
	});

	it('returns empty array when minDate or maxDate is null', () => {
		expect(buildMonthSequence(null, '1915-08-01')).toEqual([]);
		expect(buildMonthSequence('1915-08-01', null)).toEqual([]);
		expect(buildMonthSequence(null, null)).toEqual([]);
	});
});

describe('fillDensityGaps', () => {
	it('returns empty array when minDate or maxDate is null', () => {
		expect(fillDensityGaps([], null, null)).toEqual([]);
	});

	it('preserves existing buckets and adds zero-count buckets for missing months', () => {
		const buckets = [
			{ month: '1915-08', count: 5 },
			{ month: '1915-11', count: 2 }
		];

		const result = fillDensityGaps(buckets, '1915-08-03', '1915-11-30');

		expect(result).toEqual([
			{ month: '1915-08', count: 5 },
			{ month: '1915-09', count: 0 },
			{ month: '1915-10', count: 0 },
			{ month: '1915-11', count: 2 }
		]);
	});

	it('returns all-zero sequence when buckets array is empty', () => {
		const result = fillDensityGaps([], '1915-08-03', '1915-10-15');

		expect(result).toEqual([
			{ month: '1915-08', count: 0 },
			{ month: '1915-09', count: 0 },
			{ month: '1915-10', count: 0 }
		]);
	});

	it('keeps results sorted chronologically even when buckets arrive out of order', () => {
		const buckets = [
			{ month: '1915-10', count: 3 },
			{ month: '1915-08', count: 1 }
		];

		const result = fillDensityGaps(buckets, '1915-08-01', '1915-10-31');

		expect(result.map((b) => b.month)).toEqual(['1915-08', '1915-09', '1915-10']);
	});
});

describe('aggregateToYears', () => {
	it('returns empty array for empty input', () => {
		expect(aggregateToYears([])).toEqual([]);
	});

	it('sums counts within the same year', () => {
		const result = aggregateToYears([
			{ month: '1915-08', count: 5 },
			{ month: '1915-09', count: 2 },
			{ month: '1915-10', count: 8 }
		]);
		expect(result).toEqual([{ month: '1915', count: 15 }]);
	});

	it('produces one bucket per distinct year, sorted chronologically', () => {
		const result = aggregateToYears([
			{ month: '1916-01', count: 3 },
			{ month: '1915-08', count: 5 },
			{ month: '1916-04', count: 7 },
			{ month: '1914-12', count: 1 }
		]);
		expect(result).toEqual([
			{ month: '1914', count: 1 },
			{ month: '1915', count: 5 },
			{ month: '1916', count: 10 }
		]);
	});
});

describe('clipBucketsToRange', () => {
	const buckets = [
		{ month: '1915-08', count: 5 },
		{ month: '1915-09', count: 2 },
		{ month: '1915-10', count: 8 },
		{ month: '1915-11', count: 3 }
	];

	it('returns the original buckets when range bounds are null', () => {
		expect(clipBucketsToRange(buckets, null, null)).toBe(buckets);
	});

	it('keeps only buckets whose month falls within the range', () => {
		expect(clipBucketsToRange(buckets, '1915-09-01', '1915-10-31')).toEqual([
			{ month: '1915-09', count: 2 },
			{ month: '1915-10', count: 8 }
		]);
	});

	it('returns an empty array when the range excludes everything', () => {
		expect(clipBucketsToRange(buckets, '1916-01-01', '1916-12-31')).toEqual([]);
	});

	it('treats partial dates correctly when bounds cross month boundaries', () => {
		expect(clipBucketsToRange(buckets, '1915-09-15', '1915-10-15')).toEqual([
			{ month: '1915-09', count: 2 },
			{ month: '1915-10', count: 8 }
		]);
	});
});

describe('selectionBoundaryFrom / To', () => {
	it('handles month labels (YYYY-MM)', () => {
		expect(selectionBoundaryFrom('1915-08')).toBe('1915-08-01');
		expect(selectionBoundaryTo('1915-08')).toBe('1915-08-31');
	});

	it('handles year labels (YYYY)', () => {
		expect(selectionBoundaryFrom('1915')).toBe('1915-01-01');
		expect(selectionBoundaryTo('1915')).toBe('1915-12-31');
	});
});

describe('tickIndicesFor', () => {
	it('returns no indices for an empty bucket list', () => {
		expect(tickIndicesFor([])).toEqual([]);
	});

	it('picks years divisible by 25 when the year span exceeds 120', () => {
		const buckets = Array.from({ length: 150 }, (_, i) => ({
			month: String(1875 + i),
			count: 1
		}));
		const ticks = tickIndicesFor(buckets);
		const labels = ticks.map((i) => buckets[i].month);
		expect(labels).toEqual(['1875', '1900', '1925', '1950', '1975', '2000']);
	});

	it('picks years divisible by 10 for medium ranges (~50 years)', () => {
		const buckets = Array.from({ length: 50 }, (_, i) => ({
			month: String(1900 + i),
			count: 1
		}));
		const ticks = tickIndicesFor(buckets);
		const labels = ticks.map((i) => buckets[i].month);
		expect(labels).toEqual(['1900', '1910', '1920', '1930', '1940']);
	});

	it('picks January boundaries for long month ranges', () => {
		const buckets = [
			{ month: '1914-08', count: 1 },
			{ month: '1914-09', count: 1 },
			{ month: '1914-10', count: 1 },
			{ month: '1914-11', count: 1 },
			{ month: '1914-12', count: 1 },
			{ month: '1915-01', count: 1 },
			{ month: '1915-02', count: 1 },
			{ month: '1915-03', count: 1 },
			{ month: '1915-04', count: 1 },
			{ month: '1915-05', count: 1 },
			{ month: '1915-06', count: 1 },
			{ month: '1915-07', count: 1 },
			{ month: '1915-08', count: 1 },
			{ month: '1915-09', count: 1 },
			{ month: '1915-10', count: 1 },
			{ month: '1915-11', count: 1 },
			{ month: '1915-12', count: 1 },
			{ month: '1916-01', count: 1 },
			{ month: '1916-02', count: 1 }
		];
		const ticks = tickIndicesFor(buckets);
		expect(ticks.map((i) => buckets[i].month)).toEqual(['1915-01', '1916-01']);
	});

	it('falls back to evenly spaced ticks for short month ranges (12 months)', () => {
		const buckets = Array.from({ length: 12 }, (_, i) => ({
			month: `1905-${String(i + 1).padStart(2, '0')}`,
			count: 1
		}));
		const ticks = tickIndicesFor(buckets);
		expect(ticks.length).toBeGreaterThanOrEqual(5);
		expect(ticks.length).toBeLessThanOrEqual(7);
		expect(ticks[0]).toBe(0);
	});
});

describe('formatTickLabel', () => {
	it('returns the year string unchanged for year labels', () => {
		expect(formatTickLabel('1905', 'en-US')).toBe('1905');
	});

	it('formats month labels with the year by default', () => {
		const result = formatTickLabel('1905-06', 'en-US');
		expect(result).toMatch(/Jun/);
		expect(result).toMatch(/1905/);
	});

	it('omits the year when omitYear is true', () => {
		const result = formatTickLabel('1905-06', 'en-US', true);
		expect(result).toMatch(/Jun/);
		expect(result).not.toMatch(/1905/);
	});
});
