// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package timeutil

import (
	"reflect"
	"testing"
	"time"

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

func TestTimeutil_StartOfPreviousMonth(t *testing.T) {
	testCases := []struct {
		Input    time.Time
		Expected time.Time
	}{
		{
			Input:    time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			Expected: time.Date(2019, 12, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 1, 15, 0, 0, 0, 0, time.UTC),
			Expected: time.Date(2019, 12, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 3, 31, 23, 59, 59, 999999999, time.UTC),
			Expected: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
		},
	}

	for _, tc := range testCases {
		result := StartOfPreviousMonth(tc.Input)
		if !result.Equal(tc.Expected) {
			t.Errorf("start of month before %v is %v, got %v", tc.Input, tc.Expected, result)
		}
	}
}

func TestTimeutil_StartOfMonth(t *testing.T) {
	testCases := []struct {
		Input    time.Time
		Expected time.Time
	}{
		{
			Input:    time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC),
			Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC),
			Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 1, 31, 23, 59, 59, 999999999, time.UTC),
			Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 2, 28, 1, 2, 3, 4, time.UTC),
			Expected: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
		},
	}

	for _, tc := range testCases {
		result := StartOfMonth(tc.Input)
		if !result.Equal(tc.Expected) {
			t.Errorf("start of %v is %v, expected %v", tc.Input, result, tc.Expected)
		}
	}
}

func TestTimeutil_IsMonthStart(t *testing.T) {
	testCases := []struct {
		input    time.Time
		expected bool
	}{
		{
			input:    time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			expected: true,
		},
		{
			input:    time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC),
			expected: false,
		},
		{
			input:    time.Date(2020, 4, 5, 0, 0, 0, 0, time.UTC),
			expected: false,
		},
		{
			input:    time.Date(2020, 1, 31, 23, 59, 59, 999999999, time.UTC),
			expected: false,
		},
	}

	for _, tc := range testCases {
		result := IsMonthStart(tc.input)
		if result != tc.expected {
			t.Errorf("is %v the start of the month? expected %t, got %t", tc.input, tc.expected, result)
		}
	}
}

func TestTimeutil_EndOfMonth(t *testing.T) {
	testCases := []struct {
		Input    time.Time
		Expected time.Time
	}{
		{
			// The current behavior does not use the nanoseconds
			// because we didn't want to clutter the result of end-of-month reporting.
			Input:    time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC),
			Expected: time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 1, 31, 23, 59, 59, 999999999, time.UTC),
			Expected: time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC),
		},
		{
			Input:    time.Date(2020, 1, 15, 1, 2, 3, 4, time.UTC),
			Expected: time.Date(2020, 1, 31, 23, 59, 59, 0, time.UTC),
		},
		{
			// Leap year
			Input:    time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
			Expected: time.Date(2020, 2, 29, 23, 59, 59, 0, time.UTC),
		},
		{
			// non-leap year
			Input:    time.Date(2100, 2, 1, 0, 0, 0, 0, time.UTC),
			Expected: time.Date(2100, 2, 28, 23, 59, 59, 0, time.UTC),
		},
	}

	for _, tc := range testCases {
		result := EndOfMonth(tc.Input)
		if !result.Equal(tc.Expected) {
			t.Errorf("end of %v is %v, expected %v", tc.Input, result, tc.Expected)
		}
	}
}

func TestTimeutil_IsPreviousMonth(t *testing.T) {
	testCases := []struct {
		tInput       time.Time
		compareInput time.Time
		expected     bool
	}{
		{
			tInput:       time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			compareInput: time.Date(2020, 1, 31, 0, 0, 0, 0, time.UTC),
			expected:     false,
		},
		{
			tInput:       time.Date(2019, 12, 31, 0, 0, 0, 0, time.UTC),
			compareInput: time.Date(2020, 1, 31, 0, 0, 0, 0, time.UTC),
			expected:     true,
		},
		{
			// leap year (false)
			tInput:       time.Date(2019, 12, 29, 10, 10, 10, 0, time.UTC),
			compareInput: time.Date(2020, 2, 29, 10, 10, 10, 0, time.UTC),
			expected:     false,
		},
		{
			// leap year (true)
			tInput:       time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			compareInput: time.Date(2020, 2, 29, 10, 10, 10, 0, time.UTC),
			expected:     true,
		},
		{
			tInput:       time.Date(2018, 5, 5, 5, 0, 0, 0, time.UTC),
			compareInput: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			expected:     false,
		},
		{
			// test normalization. want to make subtracting 1 month from 3/30/2020 doesn't yield 2/30/2020, normalized
			// to 3/1/2020
			tInput:       time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
			compareInput: time.Date(2020, 3, 30, 0, 0, 0, 0, time.UTC),
			expected:     true,
		},
	}

	for _, tc := range testCases {
		result := IsPreviousMonth(tc.tInput, tc.compareInput)
		if result != tc.expected {
			t.Errorf("%v in previous month to %v? expected %t, got %t", tc.tInput, tc.compareInput, tc.expected, result)
		}
	}
}

func TestTimeutil_IsCurrentMonth(t *testing.T) {
	now := time.Now()
	testCases := []struct {
		input    time.Time
		expected bool
	}{
		{
			input:    now,
			expected: true,
		},
		{
			input:    StartOfMonth(now).AddDate(0, 0, -1),
			expected: false,
		},
		{
			input:    EndOfMonth(now).AddDate(0, 0, -1),
			expected: true,
		},
		{
			input:    StartOfMonth(now).AddDate(-1, 0, 0),
			expected: false,
		},
	}

	for _, tc := range testCases {
		result := IsCurrentMonth(tc.input, now)
		if result != tc.expected {
			t.Errorf("invalid result. expected %t for %v", tc.expected, tc.input)
		}
	}
}

// TestTimeutil_IsCurrentDay checks if the test times equals the current day or not.
func TestTimeutil_IsCurrentDay(t *testing.T) {
	now := time.Now()
	testCases := []struct {
		input    time.Time
		expected bool
	}{
		{
			input:    now,
			expected: true,
		},
		{
			input:    StartOfDay(now).AddDate(0, 0, -1),
			expected: false,
		},
		{
			input:    StartOfDay(now).AddDate(-1, 0, 0),
			expected: false,
		},
		{
			input:    StartOfDay(now).Add(1 * time.Second),
			expected: true,
		},
		{
			input:    StartOfDay(now).Add(-1 * time.Second),
			expected: false,
		},
		{
			input:    StartOfDay(now).Add(86400), // a day is 86400 seconds
			expected: true,
		},
	}

	for _, tc := range testCases {
		result := IsCurrentDay(tc.input, now)
		if result != tc.expected {
			t.Errorf("invalid result. expected %t for %v", tc.expected, tc.input)
		}
	}
}

func TestTimeUtil_ContiguousMonths(t *testing.T) {
	testCases := []struct {
		input    []time.Time
		expected []time.Time
	}{
		{
			input: []time.Time{
				time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 2, 5, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			},
			expected: []time.Time{
				time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 2, 5, 0, 0, 0, 0, time.UTC),
			},
		},
		{
			input: []time.Time{
				time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			},
			expected: []time.Time{
				time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			},
		},
		{
			input: []time.Time{
				time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC),
			},
			expected: []time.Time{
				time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC),
			},
		},
		{
			input:    []time.Time{},
			expected: []time.Time{},
		},
		{
			input:    nil,
			expected: nil,
		},
		{
			input: []time.Time{
				time.Date(2020, 2, 2, 0, 0, 0, 0, time.UTC),
				time.Date(2020, 1, 15, 0, 0, 0, 0, time.UTC),
			},
			expected: []time.Time{
				time.Date(2020, 2, 2, 0, 0, 0, 0, time.UTC),
			},
		},
	}

	for _, tc := range testCases {
		result := GetMostRecentContiguousMonths(tc.input)

		if !reflect.DeepEqual(tc.expected, result) {
			t.Errorf("invalid contiguous segment returned. expected %v, got %v", tc.expected, result)
		}
	}
}

func TestTimeUtil_ParseTimeFromPath(t *testing.T) {
	testCases := []struct {
		input       string
		expectedOut time.Time
		expectError bool
	}{
		{
			input:       "719020800/1",
			expectedOut: time.Unix(719020800, 0).UTC(),
			expectError: false,
		},
		{
			input:       "1601415205/3",
			expectedOut: time.Unix(1601415205, 0).UTC(),
			expectError: false,
		},
		{
			input:       "baddata/3",
			expectedOut: time.Time{},
			expectError: true,
		},
	}

	for _, tc := range testCases {
		result, err := ParseTimeFromPath(tc.input)
		gotError := err != nil

		if result != tc.expectedOut {
			t.Errorf("bad timestamp on input %q. expected: %v got: %v", tc.input, tc.expectedOut, result)
		}
		if gotError != tc.expectError {
			t.Errorf("bad error status on input %q. expected error: %t, got error: %t", tc.input, tc.expectError, gotError)
		}
	}
}

// TestTimeUtil_NormalizeToYear tests NormalizeToYear function which returns the normalized input date wrt to the normal.
func TestTimeUtil_NormalizeToYear(t *testing.T) {
	testCases := []struct {
		inputDate              time.Time
		normalDate             time.Time
		expectedNormalizedDate time.Time
	}{
		{
			inputDate:              time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2024, 10, 1, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2025, 9, 29, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2025, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2025, 10, 1, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2025, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		// inputDate more than 2 years prior to normal date
		{
			inputDate:              time.Date(2022, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2023, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2022, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2024, 9, 28, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2023, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2022, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2022, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2024, 9, 30, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2020, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2024, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		// leap year test cases
		{
			inputDate:              time.Date(2020, 9, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2024, 9, 28, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2023, 9, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2025, 2, 28, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2025, 3, 2, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2028, 2, 28, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2027, 3, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2028, 2, 29, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2027, 3, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			inputDate:              time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
			normalDate:             time.Date(2028, 3, 1, 0, 0, 0, 0, time.UTC),
			expectedNormalizedDate: time.Date(2028, 3, 1, 0, 0, 0, 0, time.UTC),
		},
	}
	for _, tc := range testCases {
		normalizedDate := NormalizeToYear(tc.inputDate, tc.normalDate)
		require.Equal(t, tc.expectedNormalizedDate, normalizedDate)
	}
}

// Test_GetRandomTimeInMonth verifies that the random time generated is in the same month of input time.
func Test_GetRandomTimeInMonth(t *testing.T) {
	currYear, _, _ := time.Now().Date()
	middleOfMonth := time.Date(currYear, 8, 15, 1, 2, 3, 0, time.UTC)

	cases := []struct {
		name      string
		inputTime time.Time
	}{
		{
			name:      "input time is in the start of a month",
			inputTime: StartOfMonth(time.Now()),
		},
		{
			name:      "input time is in the middle of a month",
			inputTime: middleOfMonth,
		},
		{
			name:      "input time is in the last of a month ",
			inputTime: EndOfMonth(time.Now()),
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			randomTime, err := GetRandomTimeInMonth(tc.inputTime)
			require.NoError(t, err)
			startOfMonth := StartOfMonth(tc.inputTime)
			endOfMonth := EndOfMonth(tc.inputTime)
			if !((randomTime.After(startOfMonth) || randomTime.Equal(startOfMonth)) && (randomTime.Before(endOfMonth) || randomTime.Equal(endOfMonth))) {
				t.Fatalf("random time %v is not in the same month as input time %v", randomTime.UTC(), tc.inputTime.UTC())
			}
		})
	}
}
