// 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 (
	"net/http"
	"strings"
	"testing"
	"time"

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

func TestClientDAVx5Flow(t *testing.T) {
	t.Run("Full DAVx5 sync flow", func(t *testing.T) {
		e := setupTestEnv(t)

		// Step 1: Discover principal
		// DAVx5 sends PROPFIND to the server root or well-known URL
		rec := caldavPROPFIND(t, e, "/dav/", "0", PropfindCurrentUserPrincipal)
		assert.True(t, rec.Code == 207 || rec.Code == 301,
			"Step 1: PROPFIND /dav/ should return 207 or redirect. Got %d", rec.Code)

		// Step 2: Get calendar-home-set from principal
		rec = caldavPROPFIND(t, e, "/dav/principals/user15/", "0", PropfindCalendarHomeSet)
		assertResponseStatus(t, rec, 207)
		assert.Contains(t, rec.Body.String(), "calendar-home-set",
			"Step 2: Principal should advertise calendar-home-set")

		// Step 3: List calendars
		rec = caldavPROPFIND(t, e, "/dav/projects", "1", PropfindCalendarCollectionProperties)
		assertResponseStatus(t, rec, 207)
		ms := parseMultistatus(t, rec)
		assert.GreaterOrEqual(t, len(ms.Responses), 2,
			"Step 3: Should list calendars")

		// Step 4: Check CTag for a specific calendar
		rec = caldavPROPFIND(t, e, "/dav/projects/36", "0", PropfindCalendarCollectionProperties)
		assertResponseStatus(t, rec, 207)

		// Step 5: Full sync — calendar-query to get all task ETags
		rec = caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
		assertResponseStatus(t, rec, 207)
		ms = parseMultistatus(t, rec)
		assert.NotEmpty(t, ms.Responses,
			"Step 5: calendar-query should return tasks")

		// Collect hrefs for multiget
		var hrefs []string
		for _, r := range ms.Responses {
			if strings.HasSuffix(r.Href, ".ics") {
				hrefs = append(hrefs, r.Href)
			}
		}

		// Step 6: Multiget to fetch specific tasks
		if len(hrefs) > 0 {
			body := ReportCalendarMultiget(hrefs[:1]...) // Just fetch first task
			rec = caldavREPORT(t, e, "/dav/projects/36", body)
			assertResponseStatus(t, rec, 207)
			ms = parseMultistatus(t, rec)
			assert.Len(t, ms.Responses, 1,
				"Step 6: multiget should return requested task")
		}

		// Step 7: Push a local change via PUT
		vtodo := NewVTodo("davx5-sync-test", "DAVx5 Synced Task").
			Due(time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)).
			Build()
		rec = caldavPUT(t, e, "/dav/projects/36/davx5-sync-test.ics", vtodo)
		assert.Equal(t, http.StatusCreated, rec.Code,
			"Step 7: PUT should create the task")
	})
}

func TestClientThunderbirdFlow(t *testing.T) {
	t.Run("Thunderbird discovery and initial sync", func(t *testing.T) {
		e := setupTestEnv(t)

		// Step 1: Thunderbird starts with OPTIONS to check DAV support
		rec := caldavOPTIONS(t, e, "/dav/")
		assert.Equal(t, http.StatusOK, rec.Code,
			"Step 1: OPTIONS should succeed")
		davHeader := rec.Header().Get("DAV")
		assert.NotEmpty(t, davHeader,
			"Step 1: Should have DAV header")

		// Step 2: PROPFIND on well-known for principal
		rec = caldavRequest(t, e, "PROPFIND", "/.well-known/caldav", PropfindCurrentUserPrincipal, map[string]string{
			"Depth": "0",
		})
		assert.True(t, rec.Code == 207 || rec.Code == 301 || rec.Code == 302,
			"Step 2: well-known should respond. Got %d", rec.Code)

		// Step 3: PROPFIND principal for calendar-home-set
		rec = caldavPROPFIND(t, e, "/dav/principals/user15/", "0", PropfindCalendarHomeSet)
		assertResponseStatus(t, rec, 207)

		// Step 4: Thunderbird checks current-user-privilege-set to know if it can write
		// RFC 3744 §5.4 (rfc3744.txt line 1158)
		rec = caldavPROPFIND(t, e, "/dav/projects/36", "0", PropfindCurrentUserPrivilegeSet)
		// This may return 207 with or without the property — document the behavior
		assert.True(t, rec.Code == 207 || rec.Code == 200,
			"Step 4: PROPFIND for privileges should not error. Got %d", rec.Code)

		// Step 5: List calendars
		rec = caldavPROPFIND(t, e, "/dav/projects", "1", PropfindCalendarCollectionProperties)
		assertResponseStatus(t, rec, 207)

		// Step 6: Sync via calendar-query
		rec = caldavREPORT(t, e, "/dav/projects/36", ReportCalendarQuery)
		assertResponseStatus(t, rec, 207)
	})
}

func TestClientTasksOrgSubtasks(t *testing.T) {
	t.Run("Tasks.org subtask sync: child-only RELATED-TO", func(t *testing.T) {
		// Tasks.org behavior:
		// - Child tasks include RELATED-TO;RELTYPE=PARENT:<parent-uid>
		// - Parent tasks have NO RELATED-TO at all
		// - Tasks may arrive in any order
		// - On re-sync, parent is sent again without RELATED-TO

		e := setupTestEnv(t)

		// Round 1: Initial sync — parent first, then children
		parent := NewVTodo("tasks-org-parent", "Buy groceries").Build()
		rec := caldavPUT(t, e, "/dav/projects/36/tasks-org-parent.ics", parent)
		require.Equal(t, 201, rec.Code)

		child1 := NewVTodo("tasks-org-child-1", "Buy milk").
			RelatedToParent("tasks-org-parent").Build()
		rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-child-1.ics", child1)
		require.Equal(t, 201, rec.Code)

		child2 := NewVTodo("tasks-org-child-2", "Buy eggs").
			RelatedToParent("tasks-org-parent").Build()
		rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-child-2.ics", child2)
		require.Equal(t, 201, rec.Code)

		// Verify parent shows children
		rec = caldavGET(t, e, "/dav/projects/36/tasks-org-parent.ics")
		body := rec.Body.String()
		assert.Contains(t, body, "tasks-org-child-1")
		assert.Contains(t, body, "tasks-org-child-2")

		// Round 2: Re-sync — parent updated (title change), still no RELATED-TO
		parentUpdated := NewVTodo("tasks-org-parent", "Buy groceries (updated list)").Build()
		rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-parent.ics", parentUpdated)
		require.True(t, rec.Code >= 200 && rec.Code < 300)

		// Verify children are still linked after parent re-sync
		rec = caldavGET(t, e, "/dav/projects/36/tasks-org-parent.ics")
		body = rec.Body.String()
		assert.Contains(t, body, "Buy groceries (updated list)",
			"Parent title should be updated")
		assert.Contains(t, body, "tasks-org-child-1",
			"Child 1 relation should survive parent re-sync")
		assert.Contains(t, body, "tasks-org-child-2",
			"Child 2 relation should survive parent re-sync")

		// Round 3: Complete child via PUT with STATUS:COMPLETED
		child1Done := NewVTodo("tasks-org-child-1", "Buy milk").
			RelatedToParent("tasks-org-parent").
			Status("COMPLETED").
			Completed(time.Now().UTC()).
			Build()
		rec = caldavPUT(t, e, "/dav/projects/36/tasks-org-child-1.ics", child1Done)
		require.True(t, rec.Code >= 200 && rec.Code < 300)

		// Verify child is completed
		rec = caldavGET(t, e, "/dav/projects/36/tasks-org-child-1.ics")
		assert.Contains(t, rec.Body.String(), "STATUS:COMPLETED")
	})

	t.Run("Tasks.org subtask sync: children arrive before parent", func(t *testing.T) {
		e := setupTestEnv(t)

		// Children arrive first (reverse order)
		child := NewVTodo("tasks-rev-child", "Subtask").
			RelatedToParent("tasks-rev-parent").Build()
		rec := caldavPUT(t, e, "/dav/projects/36/tasks-rev-child.ics", child)
		require.Equal(t, 201, rec.Code)

		// Parent arrives later — no RELATED-TO
		parent := NewVTodo("tasks-rev-parent", "Main Task").Build()
		rec = caldavPUT(t, e, "/dav/projects/36/tasks-rev-parent.ics", parent)
		require.Equal(t, 201, rec.Code)

		// Verify bidirectional relations
		rec = caldavGET(t, e, "/dav/projects/36/tasks-rev-parent.ics")
		assert.Contains(t, rec.Body.String(), "SUMMARY:Main Task",
			"Parent should have real title, not DUMMY")
		assert.Contains(t, rec.Body.String(), "tasks-rev-child",
			"Parent should show child relation")

		rec = caldavGET(t, e, "/dav/projects/36/tasks-rev-child.ics")
		assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:tasks-rev-parent")
	})
}
