package executorext

import (
	"context"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor/mocks"
)

func makeExecTaskStatusEx(taskId string) *executor.ExecTaskStatusEx {
	return &executor.ExecTaskStatusEx{
		ExecTaskStatus: executor.ExecTaskStatus{
			ExecTask: &executor.ExecutorTask{
				ExecId: taskId,
			},
		},
	}
}

func TestDagInstanceFinder_GetRunningDagInstancesWithFilterFunc(t *testing.T) {
	t.Run("successfully returns matched dag instances", func(t *testing.T) {
		mockExecutor := mocks.NewRemoteExecutor(t)
		finder := NewDagInstanceFinder(mockExecutor)

		filters := []DagInsFilter{
			{ArgKey: "cluster_id", ArgValues: []string{"123"}},
		}
		expected1 := executor.ExecTaskIdStatus{
			"task-1": makeExecTaskStatusEx("task-1"),
		}
		expected2 := executor.ExecTaskIdStatus{
			"task-2": makeExecTaskStatusEx("task-2"),
		}

		mockExecutor.On("QueryRunnings", mock.Anything, "planA", mock.Anything).Return(expected1, nil)
		mockExecutor.On("QueryRunnings", mock.Anything, "planB", mock.Anything).Return(expected2, nil)

		result, err := finder.GetRunningDagInstancesWithFilterFunc(context.Background(), []string{"planA", "planB"}, matchAllFiltersFun, filters...)
		require.NoError(t, err)
		assert.Equal(t, makeExecTaskStatusEx("task-1"), result["task-1"])
		assert.Equal(t, makeExecTaskStatusEx("task-2"), result["task-2"])

		mockExecutor.AssertExpectations(t)
	})

	t.Run("return error if QueryRunnings failed", func(t *testing.T) {
		mockExecutor := mocks.NewRemoteExecutor(t)
		finder := NewDagInstanceFinder(mockExecutor)

		filters := []DagInsFilter{
			{ArgKey: "cluster_id", ArgValues: []string{"123"}},
		}

		queryErr := fmt.Errorf("query error")
		mockExecutor.On("QueryRunnings", mock.Anything, "planA", mock.Anything).Return(nil, queryErr)

		result, err := finder.GetRunningDagInstancesWithFilterFunc(context.Background(), []string{"planA"}, matchAllFiltersFun, filters...)
		require.ErrorIs(t, err, queryErr)
		assert.Nil(t, result)

		mockExecutor.AssertExpectations(t)
	})

	t.Run("returns error if filter validation fails", func(t *testing.T) {
		finder := NewDagInstanceFinder(nil)

		invalidFilters := []DagInsFilter{
			{ArgKey: "", ArgValues: []string{"123"}},
		}

		_, err := finder.GetRunningDagInstancesWithFilterFunc(context.Background(), []string{"planA"}, matchAllFiltersFun, invalidFilters...)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "invalid filter.ArgKey")
	})
}

func TestDagInstanceFinder_GetRunningDagInstance(t *testing.T) {
	t.Run("returns error if query failed", func(t *testing.T) {
		mockExecutor := mocks.NewRemoteExecutor(t)
		finder := NewDagInstanceFinder(mockExecutor)

		queryErr := fmt.Errorf("query error")
		mockExecutor.On("QueryRunnings", mock.Anything, "planA", mock.Anything).Return(nil, queryErr)

		_, err := finder.GetRunningDagInstance(context.Background(), "planA", DagInsFilter{
			ArgKey: "cluster_id", ArgValues: []string{"123"},
		})

		require.ErrorIs(t, err, queryErr)
		mockExecutor.AssertExpectations(t)
	})

	t.Run("returns error if multiple dag instances are found", func(t *testing.T) {
		mockExecutor := mocks.NewRemoteExecutor(t)
		finder := NewDagInstanceFinder(mockExecutor)

		mockExecutor.On("QueryRunnings", mock.Anything, "planA", mock.Anything).Return(executor.ExecTaskIdStatus{
			"id1": {}, "id2": {},
		}, nil)

		_, err := finder.GetRunningDagInstance(context.Background(), "planA", DagInsFilter{
			ArgKey: "cluster_id", ArgValues: []string{"123"},
		})
		require.Error(t, err)
		assert.Contains(t, err.Error(), "two dag ins")
		mockExecutor.AssertExpectations(t)
	})

	t.Run("returns error if no dag instance is found", func(t *testing.T) {
		mockExecutor := mocks.NewRemoteExecutor(t)
		finder := NewDagInstanceFinder(mockExecutor)

		mockExecutor.On("QueryRunnings", mock.Anything, "planA", mock.Anything).Return(executor.ExecTaskIdStatus{}, nil)

		result, err := finder.GetRunningDagInstance(context.Background(), "planA", DagInsFilter{
			ArgKey: "cluster_id", ArgValues: []string{"123"},
		})

		assert.Nil(t, result)
		require.ErrorIs(t, err, DagInsNotFoundErr)
		mockExecutor.AssertExpectations(t)
	})

	t.Run("returns single matching dag instance", func(t *testing.T) {
		mockExecutor := mocks.NewRemoteExecutor(t)
		finder := NewDagInstanceFinder(mockExecutor)

		expected := executor.ExecTaskIdStatus{
			"task-1": makeExecTaskStatusEx("task-1"),
		}
		mockExecutor.On("QueryRunnings", mock.Anything, "planA", mock.Anything).Return(expected, nil)

		result, err := finder.GetRunningDagInstance(context.Background(), "planA", DagInsFilter{
			ArgKey: "cluster_id", ArgValues: []string{"123"},
		})
		require.NoError(t, err)
		assert.Equal(t, expected["task-1"], result)

		mockExecutor.AssertExpectations(t)
	})
}

func TestValidateFilters(t *testing.T) {
	tests := []struct {
		name        string
		filters     []DagInsFilter
		expectError bool
		errorSubstr string
	}{
		{
			name: "valid filters",
			filters: []DagInsFilter{
				{ArgKey: "cluster_id", ArgValues: []string{"123"}},
				{ArgKey: "backup_id", ArgValues: []string{"456"}},
			},
			expectError: false,
		},
		{
			name: "empty ArgKey",
			filters: []DagInsFilter{
				{ArgKey: "", ArgValues: []string{"abc"}},
			},
			expectError: true,
			errorSubstr: "filter.ArgKey",
		},
		{
			name: "empty ArgValues slice",
			filters: []DagInsFilter{
				{ArgKey: "cluster_id", ArgValues: []string{}},
			},
			expectError: true,
			errorSubstr: "no value found",
		},
		{
			name: "empty value in ArgValues",
			filters: []DagInsFilter{
				{ArgKey: "cluster_id", ArgValues: []string{""}},
			},
			expectError: true,
			errorSubstr: "empty is not allowed",
		},
		{
			name: "duplicate ArgKey",
			filters: []DagInsFilter{
				{ArgKey: "cluster_id", ArgValues: []string{"1"}},
				{ArgKey: "cluster_id", ArgValues: []string{"2"}},
			},
			expectError: true,
			errorSubstr: "duplicate filter.ArgKey",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateFilters(tt.filters)
			if tt.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tt.errorSubstr)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestMatchAllFiltersFun(t *testing.T) {
	tests := []struct {
		name        string
		filters     []DagInsFilter
		args        actions.ActionArgs
		expectMatch bool
	}{
		{
			name: "all filters matched",
			filters: []DagInsFilter{
				{ArgKey: "cluster_id", ArgValues: []string{"abc", "bcd"}},
				{ArgKey: "backup_id", ArgValues: []string{"456", "hij"}},
			},
			args: actions.ActionArgs{
				"cluster_id": actions.ActionArgString("abc"),
				"backup_id":  actions.ActionArgString("456"),
			},
			expectMatch: true,
		},
		{
			name: "value mismatch",
			filters: []DagInsFilter{
				{ArgKey: "cluster_id", ArgValues: []string{"abc"}},
			},
			args: actions.ActionArgs{
				"cluster_id": actions.ActionArgString("wrong"),
			},
			expectMatch: false,
		},
		{
			name: "missing key in args",
			filters: []DagInsFilter{
				{ArgKey: "backup_id", ArgValues: []string{"123"}},
			},
			args:        actions.ActionArgs{},
			expectMatch: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mf := matchAllFiltersFun(tt.filters)
			matchFunc := mf.ToMatchFunc()
			ok, err := matchFunc(tt.args)

			require.NoError(t, err)
			assert.Equal(t, tt.expectMatch, ok)
		})
	}
}
