package costexplorer

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

func TestSplitDateRangeByDay(t *testing.T) {
	loc := time.UTC

	t.Run("single day exact boundaries", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 0, 0, 0, 0, loc)
		end := time.Date(2025, 6, 11, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 1)
		assert.Equal(t, start, ranges[0].start)
		assert.Equal(t, end, ranges[0].end)
	})

	t.Run("multi-day range 3 days", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 0, 0, 0, 0, loc)
		end := time.Date(2025, 6, 13, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 3)
		assert.Equal(t, time.Date(2025, 6, 10, 0, 0, 0, 0, loc), ranges[0].start)
		assert.Equal(t, time.Date(2025, 6, 11, 0, 0, 0, 0, loc), ranges[0].end)
		assert.Equal(t, time.Date(2025, 6, 11, 0, 0, 0, 0, loc), ranges[1].start)
		assert.Equal(t, time.Date(2025, 6, 12, 0, 0, 0, 0, loc), ranges[1].end)
		assert.Equal(t, time.Date(2025, 6, 12, 0, 0, 0, 0, loc), ranges[2].start)
		assert.Equal(t, time.Date(2025, 6, 13, 0, 0, 0, 0, loc), ranges[2].end)
	})

	t.Run("full month 31 days", func(t *testing.T) {
		start := time.Date(2025, 7, 1, 0, 0, 0, 0, loc)
		end := time.Date(2025, 8, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 31)
	})

	t.Run("full month 28 days February non-leap", func(t *testing.T) {
		start := time.Date(2025, 2, 1, 0, 0, 0, 0, loc)
		end := time.Date(2025, 3, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 28)
	})

	t.Run("full month 29 days February leap year", func(t *testing.T) {
		start := time.Date(2024, 2, 1, 0, 0, 0, 0, loc)
		end := time.Date(2024, 3, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 29)
	})

	t.Run("end equals start returns nil", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, start)
		assert.Nil(t, ranges)
	})

	t.Run("end before start returns nil", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 0, 0, 0, 0, loc)
		end := time.Date(2025, 6, 9, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Nil(t, ranges)
	})

	t.Run("zero time both returns nil", func(t *testing.T) {
		ranges := splitDateRangeByDay(time.Time{}, time.Time{})
		assert.Nil(t, ranges)
	})

	t.Run("partial day mid-day to mid-day preserves original boundaries", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 12, 30, 0, 0, loc)
		end := time.Date(2025, 6, 10, 18, 45, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 1)
		assert.Equal(t, start, ranges[0].start)
		assert.Equal(t, end, ranges[0].end)
	})

	t.Run("two partial days across boundary", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 12, 0, 0, 0, loc)
		end := time.Date(2025, 6, 11, 15, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 2)
		// First range preserves original start, not midnight
		assert.Equal(t, start, ranges[0].start)
		assert.Equal(t, time.Date(2025, 6, 11, 0, 0, 0, 0, loc), ranges[0].end)
		// Second range starts at midnight and preserves original end
		assert.Equal(t, time.Date(2025, 6, 11, 0, 0, 0, 0, loc), ranges[1].start)
		assert.Equal(t, end, ranges[1].end)
	})

	t.Run("day ranges are contiguous non-overlapping", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 0, 0, 0, 0, loc)
		end := time.Date(2025, 6, 20, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Greater(t, len(ranges), 1)
		for i := 1; i < len(ranges); i++ {
			assert.True(t, ranges[i-1].end.Equal(ranges[i].start),
				"gap between range %d end=%v and range %d start=%v",
				i-1, ranges[i-1].end, i, ranges[i].start)
		}
	})

	t.Run("first range preserves original start time not midnight", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 15, 30, 45, 0, loc)
		end := time.Date(2025, 6, 13, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByDay(start, end)
		assert.Greater(t, len(ranges), 0)
		assert.Equal(t, start, ranges[0].start)
		// Subsequent ranges start at midnight
		assert.Equal(t, time.Date(2025, 6, 11, 0, 0, 0, 0, loc), ranges[1].start)
	})

	t.Run("DST spring forward 23 hour day", func(t *testing.T) {
		usEastern, _ := time.LoadLocation("America/New_York")
		start := time.Date(2025, 3, 9, 0, 0, 0, 0, usEastern)
		end := time.Date(2025, 3, 11, 0, 0, 0, 0, usEastern)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 2)
	})

	t.Run("DST fall back 25 hour day", func(t *testing.T) {
		usEastern, _ := time.LoadLocation("America/New_York")
		start := time.Date(2025, 11, 2, 0, 0, 0, 0, usEastern)
		end := time.Date(2025, 11, 4, 0, 0, 0, 0, usEastern)
		ranges := splitDateRangeByDay(start, end)
		assert.Len(t, ranges, 2)
	})
}

func TestSplitDateRangeByMonth(t *testing.T) {
	loc := time.UTC

	t.Run("single month exact boundaries", func(t *testing.T) {
		start := time.Date(2025, 6, 1, 0, 0, 0, 0, loc)
		end := time.Date(2025, 7, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Len(t, ranges, 1)
		assert.Equal(t, start, ranges[0].start)
		assert.Equal(t, end, ranges[0].end)
	})

	t.Run("three months", func(t *testing.T) {
		start := time.Date(2025, 3, 1, 0, 0, 0, 0, loc)
		end := time.Date(2025, 6, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Len(t, ranges, 3)
		assert.Equal(t, time.Date(2025, 3, 1, 0, 0, 0, 0, loc), ranges[0].start)
		assert.Equal(t, time.Date(2025, 4, 1, 0, 0, 0, 0, loc), ranges[0].end)
		assert.Equal(t, time.Date(2025, 4, 1, 0, 0, 0, 0, loc), ranges[1].start)
		assert.Equal(t, time.Date(2025, 5, 1, 0, 0, 0, 0, loc), ranges[1].end)
		assert.Equal(t, time.Date(2025, 5, 1, 0, 0, 0, 0, loc), ranges[2].start)
		assert.Equal(t, time.Date(2025, 6, 1, 0, 0, 0, 0, loc), ranges[2].end)
	})

	t.Run("full year 12 months", func(t *testing.T) {
		start := time.Date(2025, 1, 1, 0, 0, 0, 0, loc)
		end := time.Date(2026, 1, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Len(t, ranges, 12)
	})

	t.Run("end equals start returns nil", func(t *testing.T) {
		start := time.Date(2025, 6, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, start)
		assert.Nil(t, ranges)
	})

	t.Run("end before start returns nil", func(t *testing.T) {
		start := time.Date(2025, 6, 1, 0, 0, 0, 0, loc)
		end := time.Date(2025, 5, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Nil(t, ranges)
	})

	t.Run("partial first month preserves original start", func(t *testing.T) {
		start := time.Date(2025, 6, 15, 0, 0, 0, 0, loc)
		end := time.Date(2025, 8, 1, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Greater(t, len(ranges), 1)
		assert.Equal(t, start, ranges[0].start)
	})

	t.Run("partial last month preserves original end", func(t *testing.T) {
		start := time.Date(2025, 6, 1, 0, 0, 0, 0, loc)
		end := time.Date(2025, 8, 15, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Greater(t, len(ranges), 1)
		assert.Equal(t, end, ranges[len(ranges)-1].end)
	})

	t.Run("45 days spanning two months", func(t *testing.T) {
		start := time.Date(2025, 6, 10, 0, 0, 0, 0, loc)
		end := time.Date(2025, 7, 25, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Len(t, ranges, 2)
		assert.Equal(t, start, ranges[0].start)
		assert.Equal(t, time.Date(2025, 7, 1, 0, 0, 0, 0, loc), ranges[0].end)
		assert.Equal(t, time.Date(2025, 7, 1, 0, 0, 0, 0, loc), ranges[1].start)
		assert.Equal(t, end, ranges[1].end)
	})

	t.Run("month ranges are contiguous non-overlapping", func(t *testing.T) {
		start := time.Date(2025, 1, 10, 0, 0, 0, 0, loc)
		end := time.Date(2025, 12, 20, 0, 0, 0, 0, loc)
		ranges := splitDateRangeByMonth(start, end)
		assert.Greater(t, len(ranges), 1)
		for i := 1; i < len(ranges); i++ {
			assert.True(t, ranges[i-1].end.Equal(ranges[i].start),
				"gap between range %d end=%v and range %d start=%v",
				i-1, ranges[i-1].end, i, ranges[i].start)
		}
	})
}
