// Implementation of TOML's local date/time.
//
// Copied over from Google's civil to avoid pulling all the Google dependencies.
// Originals:
//   https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil_test.go
// Changes:
//   * Renamed files from civil* to localtime*.
//   * Package changed from civil to toml.
//   * 'Local' prefix added to all structs.
//
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package toml

import (
	"encoding/json"
	"reflect"
	"testing"
	"time"
)

func cmpEqual(x, y interface{}) bool {
	return reflect.DeepEqual(x, y)
}

func TestDates(t *testing.T) {
	for _, test := range []struct {
		date     LocalDate
		loc      *time.Location
		wantStr  string
		wantTime time.Time
	}{
		{
			date:     LocalDate{2014, 7, 29},
			loc:      time.Local,
			wantStr:  "2014-07-29",
			wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local),
		},
		{
			date:     LocalDateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)),
			loc:      time.UTC,
			wantStr:  "2014-08-20",
			wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC),
		},
		{
			date:     LocalDateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)),
			loc:      time.UTC,
			wantStr:  "0999-01-26",
			wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC),
		},
	} {
		if got := test.date.String(); got != test.wantStr {
			t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr)
		}
		if got := test.date.In(test.loc); !got.Equal(test.wantTime) {
			t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime)
		}
	}
}

func TestDateIsValid(t *testing.T) {
	for _, test := range []struct {
		date LocalDate
		want bool
	}{
		{LocalDate{2014, 7, 29}, true},
		{LocalDate{2000, 2, 29}, true},
		{LocalDate{10000, 12, 31}, true},
		{LocalDate{1, 1, 1}, true},
		{LocalDate{0, 1, 1}, true},  // year zero is OK
		{LocalDate{-1, 1, 1}, true}, // negative year is OK
		{LocalDate{1, 0, 1}, false},
		{LocalDate{1, 1, 0}, false},
		{LocalDate{2016, 1, 32}, false},
		{LocalDate{2016, 13, 1}, false},
		{LocalDate{1, -1, 1}, false},
		{LocalDate{1, 1, -1}, false},
	} {
		got := test.date.IsValid()
		if got != test.want {
			t.Errorf("%#v: got %t, want %t", test.date, got, test.want)
		}
	}
}

func TestParseDate(t *testing.T) {
	for _, test := range []struct {
		str  string
		want LocalDate // if empty, expect an error
	}{
		{"2016-01-02", LocalDate{2016, 1, 2}},
		{"2016-12-31", LocalDate{2016, 12, 31}},
		{"0003-02-04", LocalDate{3, 2, 4}},
		{"999-01-26", LocalDate{}},
		{"", LocalDate{}},
		{"2016-01-02x", LocalDate{}},
	} {
		got, err := ParseLocalDate(test.str)
		if got != test.want {
			t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want)
		}
		if err != nil && test.want != (LocalDate{}) {
			t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str)
		}
	}
}

func TestDateArithmetic(t *testing.T) {
	for _, test := range []struct {
		desc  string
		start LocalDate
		end   LocalDate
		days  int
	}{
		{
			desc:  "zero days noop",
			start: LocalDate{2014, 5, 9},
			end:   LocalDate{2014, 5, 9},
			days:  0,
		},
		{
			desc:  "crossing a year boundary",
			start: LocalDate{2014, 12, 31},
			end:   LocalDate{2015, 1, 1},
			days:  1,
		},
		{
			desc:  "negative number of days",
			start: LocalDate{2015, 1, 1},
			end:   LocalDate{2014, 12, 31},
			days:  -1,
		},
		{
			desc:  "full leap year",
			start: LocalDate{2004, 1, 1},
			end:   LocalDate{2005, 1, 1},
			days:  366,
		},
		{
			desc:  "full non-leap year",
			start: LocalDate{2001, 1, 1},
			end:   LocalDate{2002, 1, 1},
			days:  365,
		},
		{
			desc:  "crossing a leap second",
			start: LocalDate{1972, 6, 30},
			end:   LocalDate{1972, 7, 1},
			days:  1,
		},
		{
			desc:  "dates before the unix epoch",
			start: LocalDate{101, 1, 1},
			end:   LocalDate{102, 1, 1},
			days:  365,
		},
	} {
		if got := test.start.AddDays(test.days); got != test.end {
			t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end)
		}
		if got := test.end.DaysSince(test.start); got != test.days {
			t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days)
		}
	}
}

func TestDateBefore(t *testing.T) {
	for _, test := range []struct {
		d1, d2 LocalDate
		want   bool
	}{
		{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, true},
		{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
		{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, true},
		{LocalDate{2016, 1, 30}, LocalDate{2016, 12, 31}, true},
	} {
		if got := test.d1.Before(test.d2); got != test.want {
			t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want)
		}
	}
}

func TestDateAfter(t *testing.T) {
	for _, test := range []struct {
		d1, d2 LocalDate
		want   bool
	}{
		{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, false},
		{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
		{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, false},
	} {
		if got := test.d1.After(test.d2); got != test.want {
			t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want)
		}
	}
}

func TestTimeToString(t *testing.T) {
	for _, test := range []struct {
		str       string
		time      LocalTime
		roundTrip bool // ParseLocalTime(str).String() == str?
	}{
		{"13:26:33", LocalTime{13, 26, 33, 0}, true},
		{"01:02:03.000023456", LocalTime{1, 2, 3, 23456}, true},
		{"00:00:00.000000001", LocalTime{0, 0, 0, 1}, true},
		{"13:26:03.1", LocalTime{13, 26, 3, 100000000}, false},
		{"13:26:33.0000003", LocalTime{13, 26, 33, 300}, false},
	} {
		gotTime, err := ParseLocalTime(test.str)
		if err != nil {
			t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err)
			continue
		}
		if gotTime != test.time {
			t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time)
		}
		if test.roundTrip {
			gotStr := test.time.String()
			if gotStr != test.str {
				t.Errorf("%#v.String() = %q, want %q", test.time, gotStr, test.str)
			}
		}
	}
}

func TestTimeOf(t *testing.T) {
	for _, test := range []struct {
		time time.Time
		want LocalTime
	}{
		{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), LocalTime{15, 8, 43, 1}},
		{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), LocalTime{0, 0, 0, 0}},
	} {
		if got := LocalTimeOf(test.time); got != test.want {
			t.Errorf("LocalTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
		}
	}
}

func TestTimeIsValid(t *testing.T) {
	for _, test := range []struct {
		time LocalTime
		want bool
	}{
		{LocalTime{0, 0, 0, 0}, true},
		{LocalTime{23, 0, 0, 0}, true},
		{LocalTime{23, 59, 59, 999999999}, true},
		{LocalTime{24, 59, 59, 999999999}, false},
		{LocalTime{23, 60, 59, 999999999}, false},
		{LocalTime{23, 59, 60, 999999999}, false},
		{LocalTime{23, 59, 59, 1000000000}, false},
		{LocalTime{-1, 0, 0, 0}, false},
		{LocalTime{0, -1, 0, 0}, false},
		{LocalTime{0, 0, -1, 0}, false},
		{LocalTime{0, 0, 0, -1}, false},
	} {
		got := test.time.IsValid()
		if got != test.want {
			t.Errorf("%#v: got %t, want %t", test.time, got, test.want)
		}
	}
}

func TestDateTimeToString(t *testing.T) {
	for _, test := range []struct {
		str       string
		dateTime  LocalDateTime
		roundTrip bool // ParseLocalDateTime(str).String() == str?
	}{
		{"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, true},
		{"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 600}}, true},
		{"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, false},
	} {
		gotDateTime, err := ParseLocalDateTime(test.str)
		if err != nil {
			t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err)
			continue
		}
		if gotDateTime != test.dateTime {
			t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime)
		}
		if test.roundTrip {
			gotStr := test.dateTime.String()
			if gotStr != test.str {
				t.Errorf("%#v.String() = %q, want %q", test.dateTime, gotStr, test.str)
			}
		}
	}
}

func TestParseDateTimeErrors(t *testing.T) {
	for _, str := range []string{
		"",
		"2016-03-22",           // just a date
		"13:26:33",             // just a time
		"2016-03-22 13:26:33",  // wrong separating character
		"2016-03-22T13:26:33x", // extra at end
	} {
		if _, err := ParseLocalDateTime(str); err == nil {
			t.Errorf("ParseLocalDateTime(%q) succeeded, want error", str)
		}
	}
}

func TestDateTimeOf(t *testing.T) {
	for _, test := range []struct {
		time time.Time
		want LocalDateTime
	}{
		{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local),
			LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}}},
		{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
			LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}}},
	} {
		if got := LocalDateTimeOf(test.time); got != test.want {
			t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
		}
	}
}

func TestDateTimeIsValid(t *testing.T) {
	// No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid.
	for _, test := range []struct {
		dt   LocalDateTime
		want bool
	}{
		{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{0, 0, 0, 0}}, true},
		{LocalDateTime{LocalDate{2016, -3, 20}, LocalTime{0, 0, 0, 0}}, false},
		{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{24, 0, 0, 0}}, false},
	} {
		got := test.dt.IsValid()
		if got != test.want {
			t.Errorf("%#v: got %t, want %t", test.dt, got, test.want)
		}
	}
}

func TestDateTimeIn(t *testing.T) {
	dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}}
	got := dt.In(time.UTC)
	want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC)
	if !got.Equal(want) {
		t.Errorf("got %v, want %v", got, want)
	}
}

func TestDateTimeBefore(t *testing.T) {
	d1 := LocalDate{2016, 12, 31}
	d2 := LocalDate{2017, 1, 1}
	t1 := LocalTime{5, 6, 7, 8}
	t2 := LocalTime{5, 6, 7, 9}
	for _, test := range []struct {
		dt1, dt2 LocalDateTime
		want     bool
	}{
		{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, true},
		{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, true},
		{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, false},
		{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
	} {
		if got := test.dt1.Before(test.dt2); got != test.want {
			t.Errorf("%v.Before(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
		}
	}
}

func TestDateTimeAfter(t *testing.T) {
	d1 := LocalDate{2016, 12, 31}
	d2 := LocalDate{2017, 1, 1}
	t1 := LocalTime{5, 6, 7, 8}
	t2 := LocalTime{5, 6, 7, 9}
	for _, test := range []struct {
		dt1, dt2 LocalDateTime
		want     bool
	}{
		{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, false},
		{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, false},
		{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, true},
		{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
	} {
		if got := test.dt1.After(test.dt2); got != test.want {
			t.Errorf("%v.After(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
		}
	}
}

func TestMarshalJSON(t *testing.T) {
	for _, test := range []struct {
		value interface{}
		want  string
	}{
		{LocalDate{1987, 4, 15}, `"1987-04-15"`},
		{LocalTime{18, 54, 2, 0}, `"18:54:02"`},
		{LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}, `"1987-04-15T18:54:02"`},
	} {
		bgot, err := json.Marshal(test.value)
		if err != nil {
			t.Fatal(err)
		}
		if got := string(bgot); got != test.want {
			t.Errorf("%#v: got %s, want %s", test.value, got, test.want)
		}
	}
}

func TestUnmarshalJSON(t *testing.T) {
	var d LocalDate
	var tm LocalTime
	var dt LocalDateTime
	for _, test := range []struct {
		data string
		ptr  interface{}
		want interface{}
	}{
		{`"1987-04-15"`, &d, &LocalDate{1987, 4, 15}},
		{`"1987-04-\u0031\u0035"`, &d, &LocalDate{1987, 4, 15}},
		{`"18:54:02"`, &tm, &LocalTime{18, 54, 2, 0}},
		{`"1987-04-15T18:54:02"`, &dt, &LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}},
	} {
		if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil {
			t.Fatalf("%s: %v", test.data, err)
		}
		if !cmpEqual(test.ptr, test.want) {
			t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want)
		}
	}

	for _, bad := range []string{"", `""`, `"bad"`, `"1987-04-15x"`,
		`19870415`,     // a JSON number
		`11987-04-15x`, // not a JSON string

	} {
		if json.Unmarshal([]byte(bad), &d) == nil {
			t.Errorf("%q, LocalDate: got nil, want error", bad)
		}
		if json.Unmarshal([]byte(bad), &tm) == nil {
			t.Errorf("%q, LocalTime: got nil, want error", bad)
		}
		if json.Unmarshal([]byte(bad), &dt) == nil {
			t.Errorf("%q, LocalDateTime: got nil, want error", bad)
		}
	}
}
