// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package caldavtests

import (
	"strings"
	"testing"
	"time"

	ics "github.com/arran4/golang-ical"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestVTodoRoundTrip(t *testing.T) {
	// Helper: PUT a VTODO, GET it back, parse the VTODO
	putAndGet := func(t *testing.T, _, path string, vtodoBody string) *ics.VTodo {
		t.Helper()
		e := setupTestEnv(t)

		rec := caldavPUT(t, e, path, vtodoBody)
		require.True(t, rec.Code >= 200 && rec.Code < 300,
			"PUT failed with status %d. Body:\n%s", rec.Code, rec.Body.String())

		rec2 := caldavGET(t, e, path)
		require.Equal(t, 200, rec2.Code, "GET failed. Body:\n%s", rec2.Body.String())

		cal := parseICalFromResponse(t, rec2)
		return getVTodo(t, cal)
	}

	t.Run("SUMMARY round-trips", func(t *testing.T) {
		// RFC 5545 §3.8.1.12 (rfc5545.txt line 5179)
		vtodo := NewVTodo("rt-summary", "My Task Summary").Build()
		result := putAndGet(t, "rt-summary", "/dav/projects/36/rt-summary.ics", vtodo)
		assert.Equal(t, "My Task Summary", getVTodoProperty(result, ics.ComponentPropertySummary))
	})

	t.Run("DESCRIPTION round-trips", func(t *testing.T) {
		// RFC 5545 §3.8.1.5 (rfc5545.txt line 4688)
		vtodo := NewVTodo("rt-desc", "Desc Test").
			Description("This is a detailed description").
			Build()
		result := putAndGet(t, "rt-desc", "/dav/projects/36/rt-desc.ics", vtodo)
		desc := getVTodoProperty(result, ics.ComponentPropertyDescription)
		assert.Contains(t, desc, "This is a detailed description")
	})

	t.Run("DESCRIPTION with newlines round-trips", func(t *testing.T) {
		vtodo := NewVTodo("rt-desc-nl", "Desc Newline Test").
			Description("Line 1\\nLine 2\\nLine 3").
			Build()
		result := putAndGet(t, "rt-desc-nl", "/dav/projects/36/rt-desc-nl.ics", vtodo)
		desc := getVTodoProperty(result, ics.ComponentPropertyDescription)
		// Should preserve the newline structure
		assert.True(t, strings.Contains(desc, "Line 1") && strings.Contains(desc, "Line 2"),
			"Description should preserve multi-line content. Got: %s", desc)
	})

	t.Run("DUE round-trips", func(t *testing.T) {
		// RFC 5545 §3.8.2.3 (rfc5545.txt line 5353)
		due := time.Date(2024, 6, 15, 14, 30, 0, 0, time.UTC)
		vtodo := NewVTodo("rt-due", "Due Test").Due(due).Build()
		result := putAndGet(t, "rt-due", "/dav/projects/36/rt-due.ics", vtodo)

		dueStr := getVTodoProperty(result, ics.ComponentPropertyDue)
		assert.Contains(t, dueStr, "20240615",
			"DUE date should be preserved. Got: %s", dueStr)
	})

	t.Run("DTSTART round-trips", func(t *testing.T) {
		// RFC 5545 §3.8.2.4 (rfc5545.txt line 5415)
		start := time.Date(2024, 5, 1, 9, 0, 0, 0, time.UTC)
		vtodo := NewVTodo("rt-dtstart", "Start Test").DtStart(start).Build()
		result := putAndGet(t, "rt-dtstart", "/dav/projects/36/rt-dtstart.ics", vtodo)

		startStr := getVTodoProperty(result, ics.ComponentPropertyDtStart)
		assert.Contains(t, startStr, "20240501",
			"DTSTART should be preserved. Got: %s", startStr)
	})

	t.Run("PRIORITY round-trips", func(t *testing.T) {
		// RFC 5545 §3.8.1.9 (rfc5545.txt line 4956)
		// CalDAV priority 1 = Vikunja priority 5 (highest)
		// The round-trip may map through Vikunja's priority system
		// Vikunja maps: CalDAV 1→Vikunja 5→CalDAV 1
		vtodo := NewVTodo("rt-priority-1", "Priority 1 Test").Priority(1).Build()
		result := putAndGet(t, "rt-priority-1", "/dav/projects/36/rt-priority-1.ics", vtodo)
		assert.Equal(t, "1", getVTodoProperty(result, ics.ComponentPropertyPriority),
			"Priority 1 (highest) should round-trip")
	})

	t.Run("PRIORITY 5 round-trips as 5", func(t *testing.T) {
		// CalDAV 5 = Vikunja 2 (medium) → CalDAV 5
		vtodo := NewVTodo("rt-priority-5", "Priority 5 Test").Priority(5).Build()
		result := putAndGet(t, "rt-priority-5", "/dav/projects/36/rt-priority-5.ics", vtodo)
		assert.Equal(t, "5", getVTodoProperty(result, ics.ComponentPropertyPriority),
			"Priority 5 (medium) should round-trip")
	})

	t.Run("PRIORITY 9 round-trips as 9", func(t *testing.T) {
		// CalDAV 9 = Vikunja 1 (low) → CalDAV 9
		vtodo := NewVTodo("rt-priority-9", "Priority 9 Test").Priority(9).Build()
		result := putAndGet(t, "rt-priority-9", "/dav/projects/36/rt-priority-9.ics", vtodo)
		assert.Equal(t, "9", getVTodoProperty(result, ics.ComponentPropertyPriority),
			"Priority 9 (lowest) should round-trip")
	})

	t.Run("PRIORITY 0 (unset) round-trips", func(t *testing.T) {
		// Priority 0 means unset — should not appear in output
		vtodo := NewVTodo("rt-priority-0", "No Priority Test").Build()
		result := putAndGet(t, "rt-priority-0", "/dav/projects/36/rt-priority-0.ics", vtodo)
		priorityStr := getVTodoProperty(result, ics.ComponentPropertyPriority)
		assert.True(t, priorityStr == "" || priorityStr == "0",
			"Priority 0 (unset) should round-trip as empty or 0. Got: %s", priorityStr)
	})

	t.Run("COMPLETED and STATUS:COMPLETED round-trip", func(t *testing.T) {
		// RFC 5545 §3.8.2.1 (rfc5545.txt line 5240)
		completed := time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC)
		vtodo := NewVTodo("rt-completed", "Completed Test").
			Completed(completed).
			Status("COMPLETED").
			Build()
		result := putAndGet(t, "rt-completed", "/dav/projects/36/rt-completed.ics", vtodo)

		compStr := getVTodoProperty(result, ics.ComponentPropertyCompleted)
		assert.Contains(t, compStr, "20240315",
			"COMPLETED date should be preserved. Got: %s", compStr)

		statusStr := getVTodoProperty(result, ics.ComponentPropertyStatus)
		assert.Equal(t, "COMPLETED", statusStr,
			"STATUS should be COMPLETED when task is done")
	})

	t.Run("CATEGORIES round-trip", func(t *testing.T) {
		// RFC 5545 §3.8.1.2 (rfc5545.txt line 4520)
		vtodo := NewVTodo("rt-categories", "Categories Test").
			Categories("work", "urgent", "bug").
			Build()
		result := putAndGet(t, "rt-categories", "/dav/projects/36/rt-categories.ics", vtodo)

		catProp := result.GetProperty(ics.ComponentPropertyCategories)
		require.NotNil(t, catProp, "CATEGORIES property should be present")

		catStr := catProp.Value
		assert.Contains(t, catStr, "work", "Should contain 'work' category")
		assert.Contains(t, catStr, "urgent", "Should contain 'urgent' category")
		assert.Contains(t, catStr, "bug", "Should contain 'bug' category")
	})

	t.Run("Single CATEGORY round-trips", func(t *testing.T) {
		vtodo := NewVTodo("rt-cat-single", "Single Category").
			Categories("solo-label").
			Build()
		result := putAndGet(t, "rt-cat-single", "/dav/projects/36/rt-cat-single.ics", vtodo)

		catProp := result.GetProperty(ics.ComponentPropertyCategories)
		require.NotNil(t, catProp, "CATEGORIES property should be present")
		assert.Contains(t, catProp.Value, "solo-label")
	})

	t.Run("VALARM with absolute trigger round-trips", func(t *testing.T) {
		// RFC 5545 §3.8.6 (rfc5545.txt line 7352)
		alarmTime := time.Date(2024, 6, 15, 8, 0, 0, 0, time.UTC)
		vtodo := NewVTodo("rt-alarm-abs", "Alarm Absolute Test").
			Due(time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)).
			AlarmAbsolute(alarmTime).
			Build()
		result := putAndGet(t, "rt-alarm-abs", "/dav/projects/36/rt-alarm-abs.ics", vtodo)

		// Check that the VTODO contains a VALARM
		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		assert.Contains(t, body, "BEGIN:VALARM", "Should contain a VALARM component")
		assert.Contains(t, body, "TRIGGER", "VALARM should have a TRIGGER")
	})

	t.Run("VALARM with relative-to-start trigger round-trips", func(t *testing.T) {
		vtodo := NewVTodo("rt-alarm-rel-start", "Alarm Relative Start").
			DtStart(time.Date(2024, 6, 1, 9, 0, 0, 0, time.UTC)).
			AlarmRelativeStart("-PT15M").
			Build()
		result := putAndGet(t, "rt-alarm-rel-start", "/dav/projects/36/rt-alarm-rel-start.ics", vtodo)

		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		assert.Contains(t, body, "BEGIN:VALARM", "Should contain a VALARM component")
		assert.Contains(t, body, "TRIGGER", "VALARM should have a TRIGGER")
	})

	t.Run("VALARM with relative-to-end trigger round-trips", func(t *testing.T) {
		vtodo := NewVTodo("rt-alarm-rel-end", "Alarm Relative End").
			Due(time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)).
			AlarmRelativeEnd("-PT30M").
			Build()
		result := putAndGet(t, "rt-alarm-rel-end", "/dav/projects/36/rt-alarm-rel-end.ics", vtodo)

		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		assert.Contains(t, body, "BEGIN:VALARM", "Should contain a VALARM component")
	})

	t.Run("COLOR via X-APPLE-CALENDAR-COLOR round-trips", func(t *testing.T) {
		t.Skip("Known bug: Color field is not included in CalDAV export — Color: t.HexColor missing in parsing.go")
		vtodo := NewVTodo("rt-color", "Color Test").
			Color("#ff0000FF").
			Build()
		result := putAndGet(t, "rt-color", "/dav/projects/36/rt-color.ics", vtodo)

		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		// Vikunja should preserve the color in at least one of the color properties
		colorFound := strings.Contains(body, "ff0000") ||
			strings.Contains(body, "FF0000") ||
			strings.Contains(body, "#ff0000")
		assert.True(t, colorFound,
			"Color should be preserved in some form. Got:\n%s", body)
	})
}

func TestVTodoRRuleRoundTrip(t *testing.T) {
	// RFC 5545 §3.8.5.3 (rfc5545.txt line 6794)

	putAndGet := func(t *testing.T, _, path string, vtodoBody string) *ics.VTodo {
		t.Helper()
		e := setupTestEnv(t)
		rec := caldavPUT(t, e, path, vtodoBody)
		require.True(t, rec.Code >= 200 && rec.Code < 300,
			"PUT failed with %d", rec.Code)
		rec2 := caldavGET(t, e, path)
		require.Equal(t, 200, rec2.Code)
		cal := parseICalFromResponse(t, rec2)
		return getVTodo(t, cal)
	}

	t.Run("RRULE FREQ=DAILY round-trips", func(t *testing.T) {
		t.Skip("Known limitation: Vikunja does not round-trip RRULE via CalDAV")
		vtodo := NewVTodo("rt-rrule-daily", "Daily Repeat").
			Due(time.Date(2024, 6, 1, 9, 0, 0, 0, time.UTC)).
			Rrule("FREQ=DAILY;INTERVAL=1").
			Build()
		result := putAndGet(t, "rt-rrule-daily", "/dav/projects/36/rt-rrule-daily.ics", vtodo)

		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		assert.Contains(t, body, "RRULE", "Should contain RRULE")
		assert.Contains(t, body, "DAILY", "Should contain DAILY frequency")
	})

	t.Run("RRULE FREQ=WEEKLY round-trips", func(t *testing.T) {
		t.Skip("Known limitation: Vikunja only supports DAILY repeat mode, WEEKLY is not round-tripped")
		vtodo := NewVTodo("rt-rrule-weekly", "Weekly Repeat").
			Due(time.Date(2024, 6, 1, 9, 0, 0, 0, time.UTC)).
			Rrule("FREQ=WEEKLY;INTERVAL=2").
			Build()
		result := putAndGet(t, "rt-rrule-weekly", "/dav/projects/36/rt-rrule-weekly.ics", vtodo)

		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		assert.Contains(t, body, "RRULE", "Should contain RRULE")
		assert.Contains(t, body, "WEEKLY", "Should contain WEEKLY frequency")
	})

	t.Run("RRULE FREQ=MONTHLY round-trips", func(t *testing.T) {
		t.Skip("Known limitation: Vikunja only supports DAILY repeat mode, MONTHLY is not round-tripped")
		vtodo := NewVTodo("rt-rrule-monthly", "Monthly Repeat").
			Due(time.Date(2024, 6, 15, 9, 0, 0, 0, time.UTC)).
			Rrule("FREQ=MONTHLY;BYMONTHDAY=15").
			Build()
		result := putAndGet(t, "rt-rrule-monthly", "/dav/projects/36/rt-rrule-monthly.ics", vtodo)

		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		assert.Contains(t, body, "RRULE", "Should contain RRULE")
		assert.Contains(t, body, "MONTHLY", "Should contain MONTHLY frequency")
	})
}

func TestVTodoPriorityMapping(t *testing.T) {
	// RFC 5545 §3.8.1.9 (rfc5545.txt line 4956):
	// "A value of 0 specifies an undefined priority. A value of 1
	//  is the highest priority. A value of 9 is the lowest priority."
	//
	// Vikunja mapping (pkg/caldav/priority.go):
	// CalDAV 1 → Vikunja 5 → CalDAV 1 (DO NOW)
	// CalDAV 2 → Vikunja 4 → CalDAV 2 (Urgent)
	// CalDAV 3 → Vikunja 3 → CalDAV 3 (High)
	// CalDAV 4 → Vikunja 3 → CalDAV 3 (maps to High, LOSSY)
	// CalDAV 5 → Vikunja 2 → CalDAV 5 (Medium)
	// CalDAV 6-8 → Vikunja 1 → CalDAV 9 (maps to Low, LOSSY)
	// CalDAV 9 → Vikunja 1 → CalDAV 9 (Low)

	putAndGetPriority := func(t *testing.T, uid string, inputPriority int) string {
		t.Helper()
		e := setupTestEnv(t)
		vtodo := NewVTodo(uid, "Priority "+uid).Priority(inputPriority).Build()
		path := "/dav/projects/36/" + uid + ".ics"
		rec := caldavPUT(t, e, path, vtodo)
		require.True(t, rec.Code >= 200 && rec.Code < 300)
		rec2 := caldavGET(t, e, path)
		require.Equal(t, 200, rec2.Code)
		cal := parseICalFromResponse(t, rec2)
		return getVTodoProperty(getVTodo(t, cal), ics.ComponentPropertyPriority)
	}

	// Lossless round-trips
	t.Run("Priority 1 (highest) round-trips losslessly", func(t *testing.T) {
		assert.Equal(t, "1", putAndGetPriority(t, "p1", 1))
	})
	t.Run("Priority 2 round-trips losslessly", func(t *testing.T) {
		assert.Equal(t, "2", putAndGetPriority(t, "p2", 2))
	})
	t.Run("Priority 3 round-trips losslessly", func(t *testing.T) {
		assert.Equal(t, "3", putAndGetPriority(t, "p3", 3))
	})
	t.Run("Priority 5 round-trips losslessly", func(t *testing.T) {
		assert.Equal(t, "5", putAndGetPriority(t, "p5", 5))
	})
	t.Run("Priority 9 (lowest) round-trips losslessly", func(t *testing.T) {
		assert.Equal(t, "9", putAndGetPriority(t, "p9", 9))
	})

	// Lossy mappings (document the behavior)
	t.Run("Priority 4 maps to 3 (lossy)", func(t *testing.T) {
		result := putAndGetPriority(t, "p4", 4)
		assert.Equal(t, "3", result,
			"CalDAV priority 4 maps to Vikunja 3 (High), which exports as CalDAV 3")
	})
	t.Run("Priority 6 maps to 9 (lossy)", func(t *testing.T) {
		result := putAndGetPriority(t, "p6", 6)
		assert.Equal(t, "9", result,
			"CalDAV priority 6 maps to Vikunja 1 (Low), which exports as CalDAV 9")
	})
	t.Run("Priority 7 maps to 9 (lossy)", func(t *testing.T) {
		result := putAndGetPriority(t, "p7", 7)
		assert.Equal(t, "9", result)
	})
	t.Run("Priority 8 maps to 9 (lossy)", func(t *testing.T) {
		result := putAndGetPriority(t, "p8", 8)
		assert.Equal(t, "9", result)
	})
}

func TestVTodoDurationRoundTrip(t *testing.T) {
	// RFC 5545 §3.8.2.5 (rfc5545.txt line 5495):
	// "In a VTODO calendar component the property may be used to
	//  specify a positive duration of time that the to-do is expected
	//  to take for its completion."

	putAndGet := func(t *testing.T, uid, vtodoBody string) *ics.VTodo {
		t.Helper()
		e := setupTestEnv(t)
		path := "/dav/projects/36/" + uid + ".ics"
		rec := caldavPUT(t, e, path, vtodoBody)
		require.True(t, rec.Code >= 200 && rec.Code < 300)
		rec2 := caldavGET(t, e, path)
		require.Equal(t, 200, rec2.Code)
		cal := parseICalFromResponse(t, rec2)
		return getVTodo(t, cal)
	}

	t.Run("DTSTART + DURATION computes end date", func(t *testing.T) {
		t.Skip("Known limitation: Vikunja does not export DTEND or DURATION for VTODOs")
		// When DTSTART and DURATION are specified, Vikunja should compute
		// EndDate = DTSTART + DURATION (pkg/caldav/parsing.go:412-414)
		vtodo := NewVTodo("rt-duration", "Duration Test").
			DtStart(time.Date(2024, 6, 1, 9, 0, 0, 0, time.UTC)).
			Duration("PT2H").
			Build()
		result := putAndGet(t, "rt-duration", vtodo)

		// Vikunja stores DTSTART and EndDate (DTSTART+DURATION)
		// On export, it may output DTSTART and DTEND, or DTSTART and DURATION
		body := result.Serialize(&ics.SerializationConfiguration{MaxLength: 75, PropertyMaxLength: 75, NewLine: "\r\n"})
		hasEnd := strings.Contains(body, "DTEND") || strings.Contains(body, "DURATION")
		assert.True(t, hasEnd,
			"Should preserve end time information (via DTEND or DURATION). Got:\n%s", body)
	})

	t.Run("DTSTART + DUE are both preserved", func(t *testing.T) {
		vtodo := NewVTodo("rt-start-due", "Start and Due").
			DtStart(time.Date(2024, 6, 1, 9, 0, 0, 0, time.UTC)).
			Due(time.Date(2024, 6, 15, 17, 0, 0, 0, time.UTC)).
			Build()
		result := putAndGet(t, "rt-start-due", vtodo)

		startStr := getVTodoProperty(result, ics.ComponentPropertyDtStart)
		dueStr := getVTodoProperty(result, ics.ComponentPropertyDue)
		assert.Contains(t, startStr, "20240601", "DTSTART should be preserved")
		assert.Contains(t, dueStr, "20240615", "DUE should be preserved")
	})
}
