// 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 migration

import (
	"fmt"
	"testing"

	"code.vikunja.io/api/pkg/db"
	"code.vikunja.io/api/pkg/files"
	"code.vikunja.io/api/pkg/models"
	"code.vikunja.io/api/pkg/user"

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

func TestInsertFromStructure(t *testing.T) {
	u := &user.User{
		ID: 1,
	}
	t.Run("normal", func(t *testing.T) {
		db.LoadAndAssertFixtures(t)
		testStructure := []*models.ProjectWithTasksAndBuckets{
			{
				Project: models.Project{
					ID:          1,
					Title:       "Test1",
					Description: "Lorem Ipsum",
				},
				Tasks: []*models.TaskWithComments{
					{
						Task: models.Task{
							Title: "Task on parent",
						},
					},
				},
			},
			{
				Project: models.Project{
					Title:           "Testproject1",
					Description:     "Something",
					ParentProjectID: models.Ptr(int64(1)),
				},
				Buckets: []*models.Bucket{
					{
						ID:    1234,
						Title: "Test Bucket",
					},
				},
				Tasks: []*models.TaskWithComments{
					{
						Task: models.Task{
							Title:       "Task1",
							Description: "Lorem",
						},
					},
					{
						Task: models.Task{
							Title: "Task with related tasks",
							RelatedTasks: map[models.RelationKind][]*models.Task{
								models.RelationKindSubtask: {
									{
										Title:       "Related to task with related task",
										Description: "As subtask",
									},
								},
							},
						},
					},
					{
						Task: models.Task{
							Title: "Task with attachments",
							Attachments: []*models.TaskAttachment{
								{
									File: &files.File{
										Name:        "testfile",
										Size:        4,
										FileContent: []byte{1, 2, 3, 4},
									},
								},
							},
						},
					},
					{
						Task: models.Task{
							Title: "Task with labels",
							Labels: []*models.Label{
								{
									Title:    "Label1",
									HexColor: "ff00ff",
								},
								{
									Title:    "Label2",
									HexColor: "ff00ff",
								},
							},
						},
					},
					{
						Task: models.Task{
							Title: "Task with same label",
							Labels: []*models.Label{
								{
									Title:    "Label1",
									HexColor: "ff00ff",
								},
							},
						},
					},
					{
						Task: models.Task{
							Title:    "Task in a bucket",
							BucketID: 1234,
						},
					},
					{
						Task: models.Task{
							Title:    "Task in a nonexisting bucket",
							BucketID: 1111,
						},
					},
				},
			},
		}
		err := InsertFromStructure(testStructure, u)
		require.NoError(t, err)
		db.AssertExists(t, "projects", map[string]interface{}{
			"title":       testStructure[1].Title,
			"description": testStructure[1].Description,
		}, false)
		db.AssertExists(t, "task_buckets", map[string]interface{}{
			"task_id":   testStructure[1].Tasks[5].ID,
			"bucket_id": testStructure[1].Buckets[0].ID,
		}, false)
		db.AssertMissing(t, "task_buckets", map[string]interface{}{
			"bucket_id": 1111, // No task with that bucket should exist
		})
		db.AssertExists(t, "tasks", map[string]interface{}{
			"title": testStructure[0].Tasks[0].Title,
		}, false)
		assert.NotEqual(t, 0, testStructure[1].Tasks[0].BucketID) // Should get the default bucket
		assert.NotEqual(t, 0, testStructure[1].Tasks[6].BucketID) // Should get the default bucket
	})
	t.Run("reuses existing labels across imports", func(t *testing.T) {
		db.LoadAndAssertFixtures(t)

		makeStructure := func() []*models.ProjectWithTasksAndBuckets {
			return []*models.ProjectWithTasksAndBuckets{
				{
					Project: models.Project{Title: "Import project"},
					Tasks: []*models.TaskWithComments{
						{
							Task: models.Task{
								Title: "Task with label",
								Labels: []*models.Label{
									{Title: "Mealie", HexColor: "abcdef"},
								},
							},
						},
					},
				},
			}
		}

		require.NoError(t, InsertFromStructure(makeStructure(), u))
		require.NoError(t, InsertFromStructure(makeStructure(), u))

		s := db.NewSession()
		defer s.Close()
		count, err := s.Where("created_by_id = ? AND title = ?", u.ID, "Mealie").Count(&models.Label{})
		require.NoError(t, err)
		assert.Equal(t, int64(1), count, "second import must reuse the existing 'Mealie' label")
	})
	t.Run("does not merge into another user's label", func(t *testing.T) {
		db.LoadAndAssertFixtures(t)

		// Fixture label #3 'Label #3 - other user' is created_by_id: 2.
		// Importing the same title for user 1 must create a new, user-owned label.
		structure := []*models.ProjectWithTasksAndBuckets{
			{
				Project: models.Project{Title: "Import project"},
				Tasks: []*models.TaskWithComments{
					{
						Task: models.Task{
							Title:  "Task",
							Labels: []*models.Label{{Title: "Label #3 - other user"}},
						},
					},
				},
			},
		}
		require.NoError(t, InsertFromStructure(structure, u))

		db.AssertExists(t, "labels", map[string]interface{}{
			"title":         "Label #3 - other user",
			"created_by_id": u.ID,
		}, false)
	})
	t.Run("seeds positions when the export has none", func(t *testing.T) {
		db.LoadAndAssertFixtures(t)

		const taskCount = 100

		tasks := make([]*models.TaskWithComments, 0, taskCount)
		for i := range taskCount {
			tasks = append(tasks, &models.TaskWithComments{
				Task: models.Task{Title: fmt.Sprintf("Task %d", i)},
			})
		}
		require.NoError(t, InsertFromStructure([]*models.ProjectWithTasksAndBuckets{{
			Project: models.Project{Title: "Import project"},
			Tasks:   tasks,
		}}, u))

		s := db.NewSession()
		defer s.Close()

		project := &models.Project{}
		exists, err := s.Where("title = ?", "Import project").Get(project)
		require.NoError(t, err)
		require.True(t, exists)

		positions := []*models.TaskPosition{}
		require.NoError(t, s.
			Join("INNER", "tasks", "tasks.id = task_positions.task_id").
			Where("tasks.project_id = ?", project.ID).
			OrderBy("task_positions.project_view_id, tasks.id").
			Find(&positions))
		require.Len(t, positions, taskCount*4, "every task needs a position in all four default views")

		// Halving the lowest position for every task collapses positions towards zero and
		// reverses the import order, and recalculates the whole view on the way (#3297).
		for i, p := range positions {
			assert.GreaterOrEqualf(t, p.Position, models.MinPositionSpacing, "position %d of view %d collapsed", i, p.ProjectViewID)
			if i > 0 && positions[i-1].ProjectViewID == p.ProjectViewID {
				assert.Greaterf(t, p.Position, positions[i-1].Position, "tasks must keep their import order in view %d", p.ProjectViewID)
			}
		}
	})
	t.Run("keeps positions the export provides", func(t *testing.T) {
		db.LoadAndAssertFixtures(t)

		require.NoError(t, InsertFromStructure([]*models.ProjectWithTasksAndBuckets{{
			Project: models.Project{Title: "Import project"},
			Tasks: []*models.TaskWithComments{
				{Task: models.Task{Title: "Second", Position: 200}},
				{Task: models.Task{Title: "First", Position: 100}},
			},
		}}, u))

		s := db.NewSession()
		defer s.Close()

		for title, position := range map[string]float64{"First": 100, "Second": 200} {
			task := &models.Task{}
			exists, err := s.Where("title = ?", title).Get(task)
			require.NoError(t, err)
			require.True(t, exists)

			count, err := s.Where("task_id = ? AND position = ?", task.ID, position).Count(&models.TaskPosition{})
			require.NoError(t, err)
			assert.Equal(t, int64(4), count, "task %q must keep position %v in all views", title, position)
		}
	})
}
