package provisioning

import (
	"context"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"

	provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/util/testutil"
)

func TestIntegrationProvisioning_PullJobOwnershipProtection(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	helper := runGrafana(t)
	ctx := context.Background()

	// Create two repositories with folder targets and separate paths to avoid file conflicts
	const repo1 = "pulljob-repo-1"
	const repo2 = "pulljob-repo-2"

	// create both repos concurrently to reduce duration of this test
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		helper.CreateRepo(t, TestRepo{
			Name:   repo1,
			Path:   path.Join(helper.ProvisioningPath, "repo1"),
			Target: "folder",
			Copies: map[string]string{
				"testdata/all-panels.json": "dashboard1.json",
			},
			SkipResourceAssertions: true, // will check both at the same time below to reduce duration of this test
		})
	}()
	go func() {
		defer wg.Done()
		helper.CreateRepo(t, TestRepo{
			Name:   repo2,
			Path:   path.Join(helper.ProvisioningPath, "repo2"),
			Target: "folder",
			Copies: map[string]string{
				"testdata/timeline-demo.json": "dashboard2.json",
			},
			SkipResourceAssertions: true, // will check both at the same time below to reduce duration of this test
		})
	}()
	wg.Wait()

	require.EventuallyWithT(t, func(collect *assert.CollectT) {
		dashboards, err := helper.DashboardsV1.Resource.List(t.Context(), metav1.ListOptions{})
		if err != nil {
			collect.Errorf("could not list dashboards error: %s", err.Error())
			return
		}
		if len(dashboards.Items) != 2 {
			collect.Errorf("should have the expected dashboards after sync. got: %d. expected: %d", len(dashboards.Items), 2)
			return
		}
		folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{})
		if err != nil {
			collect.Errorf("could not list folders: error: %s", err.Error())
			return
		}
		if len(folders.Items) != 2 {
			collect.Errorf("should have the expected folders after sync. got: %d. expected: %d", len(folders.Items), 2)
			return
		}

		assert.Len(collect, dashboards.Items, 2)
		assert.Len(collect, folders.Items, 2)
	}, waitTimeoutDefault, waitIntervalDefault, "should have the expected dashboards and folders after sync")

	// Test: Pull job should fail when trying to manage resources owned by another repository
	t.Run("pull job should fail when trying to manage resources owned by another repository", func(t *testing.T) {
		// Step 1: Try to add a file with the same UID as repo1's dashboard to repo2's directory
		// This simulates a scenario where repo2 tries to manage a resource that repo1 already owns
		const allPanelsUID = "n1jR8vnnz" // UID from all-panels.json (owned by repo1)

		// Copy the same file (same UID) to repo2's directory to create ownership conflict
		conflictingFilePath := "repo2/conflicting-dashboard.json"
		helper.CopyToProvisioningPath(t, "testdata/all-panels.json", conflictingFilePath)
		printFileTree(t, helper.ProvisioningPath)

		// Step 2: Try to pull repo2 - should fail due to ownership conflict
		job := helper.TriggerJobAndWaitForComplete(t, repo2, provisioning.JobSpec{
			Action: provisioning.JobActionPull,
			Pull:   &provisioning.SyncJobOptions{},
		})

		// Step 3: Verify the job failed with ownership conflict error
		jobObj := &provisioning.Job{}
		err := runtime.DefaultUnstructuredConverter.FromUnstructured(job.Object, jobObj)
		require.NoError(t, err)

		// The job completes with "warning" state instead of "error" state when it doesn't have too many errors
		t.Logf("Job state: %s", jobObj.Status.State)
		t.Logf("Job errors: %v", jobObj.Status.Errors)

		require.Equal(t, provisioning.JobStateWarning, jobObj.Status.State, "job should complete with warnings due to ownership conflicts")
		require.NotEmpty(t, jobObj.Status.Errors, "should have error details")

		// Check that error mentions ownership conflict
		found := false
		for _, errMsg := range jobObj.Status.Errors {
			t.Logf("Error message: %s", errMsg)
			if strings.Contains(errMsg, fmt.Sprintf("managed by repo '%s'", repo1)) &&
				strings.Contains(errMsg, fmt.Sprintf("cannot be modified by repo '%s'", repo2)) {
				found = true
				break
			}
		}
		require.True(t, found, "should have ownership conflict error")

		// Step 4: Verify original resource is still owned by repo1 and unchanged
		originalDashboard, err := helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{})
		require.NoError(t, err, "original dashboard should still exist")
		require.Equal(t, repo1, originalDashboard.GetAnnotations()[utils.AnnoKeyManagerIdentity], "ownership should remain with repo1")

		// Clean up the conflicting file for subsequent tests
		err = os.Remove(filepath.Join(helper.ProvisioningPath, conflictingFilePath))
		require.NoError(t, err, "should clean up conflicting file")
	})

	// Test: Repositories should not delete resources owned by other repositories during pull
	t.Run("repositories should not delete resources owned by other repositories during pull", func(t *testing.T) {
		// Both repositories were created with their own resources (repo1 has all-panels.json, repo2 has timeline-demo.json)
		// Verify that pulling one repository doesn't affect the other's resources

		// Step 1: Verify both repositories have their own resources
		const allPanelsUID = "n1jR8vnnz" // UID from all-panels.json (repo1)
		const timelineUID = "mIJjFy8Kz"  // UID from timeline-demo.json (repo2)

		repo1Dashboard, err := helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{})
		require.NoError(t, err, "repo1's dashboard should exist")
		require.Equal(t, repo1, repo1Dashboard.GetAnnotations()[utils.AnnoKeyManagerIdentity], "should be owned by repo1")

		repo2Dashboard, err := helper.DashboardsV1.Resource.Get(ctx, timelineUID, metav1.GetOptions{})
		require.NoError(t, err, "repo2's dashboard should exist")
		require.Equal(t, repo2, repo2Dashboard.GetAnnotations()[utils.AnnoKeyManagerIdentity], "should be owned by repo2")

		// Step 2: Pull repo1 (which doesn't manage repo2's resource) - should complete successfully
		helper.SyncAndWait(t, repo1, nil)

		// Step 3: Verify that repo2's resource is still intact after repo1's pull
		persistentRepo2Dashboard, err := helper.DashboardsV1.Resource.Get(ctx, timelineUID, metav1.GetOptions{})
		require.NoError(t, err, "repo2's dashboard should still exist after repo1 pull")
		require.Equal(t, repo2, persistentRepo2Dashboard.GetAnnotations()[utils.AnnoKeyManagerIdentity], "ownership should remain with repo2")
		require.Equal(t, repo2Dashboard.GetGeneration(), persistentRepo2Dashboard.GetGeneration(), "repo2's resource should not be modified by repo1 pull")

		// Step 4: Pull repo2 and verify repo1's resource is still intact
		helper.TriggerJobAndWaitForSuccess(t, repo2, provisioning.JobSpec{
			Action: provisioning.JobActionPull,
			Pull:   &provisioning.SyncJobOptions{},
		})

		persistentRepo1Dashboard, err := helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{})
		require.NoError(t, err, "repo1's dashboard should still exist after repo2 pull")
		require.Equal(t, repo1, persistentRepo1Dashboard.GetAnnotations()[utils.AnnoKeyManagerIdentity], "ownership should remain with repo1")
		require.Equal(t, repo1Dashboard.GetGeneration(), persistentRepo1Dashboard.GetGeneration(), "repo1's resource should not be modified by repo2 pull")
	})
}
