/*
Copyright (c) 2026.

MIT License - see LICENSE file for details.
*/

package tools

import (
	"context"
	"encoding/json"
	"testing"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	corev1alpha1 "github.com/sozercan/orka/api/v1alpha1"
)

func TestCreateContainerTaskTool_Name(t *testing.T) {
	tool := &CreateContainerTaskTool{}
	if got := tool.Name(); got != createContainerTaskToolName {
		t.Errorf("Name() = %v, want %v", got, createContainerTaskToolName)
	}
}

func TestCreateContainerTaskTool_Description(t *testing.T) {
	tool := &CreateContainerTaskTool{}
	if got := tool.Description(); got == "" {
		t.Error("Description() returned empty string")
	}
}

func TestCreateContainerTaskTool_Parameters(t *testing.T) {
	tool := &CreateContainerTaskTool{}
	params := tool.Parameters()
	if params == nil {
		t.Fatal("Parameters() returned nil")
	}
	var schema map[string]any
	if err := json.Unmarshal(params, &schema); err != nil {
		t.Fatalf("Parameters() returned invalid JSON: %v", err)
	}
	if schema[jsonSchemaTypeField] != typeObject {
		t.Error("Parameters schema should have type: object")
	}
	props, ok := schema[jsonSchemaPropertiesField].(map[string]any)
	if !ok {
		t.Fatal("missing properties")
	}
	for _, key := range []string{nameField, "image", "command", "args", workspaceField, priorTaskField, namespaceField, timeoutField, priorityField, scheduleField} {
		if _, ok := props[key]; !ok {
			t.Errorf("missing %s property", key)
		}
	}
}

func newCreateContainerTaskToolCtx(fc client.Client) context.Context {
	taskCounter := 0
	tc := &ToolContext{
		Client:    fc,
		Namespace: defaultNamespace,
		GenerateTaskName: func() string {
			taskCounter++
			return testContainerTaskGeneratedName
		},
		TaskLabels: func() map[string]string {
			return map[string]string{managedByLabelValue: trueStr}
		},
		CheckTaskLimit: func() *ChatToolError { return nil },
		IncrementTasks: func() { taskCounter++ },
	}
	return WithToolContext(context.Background(), tc)
}

func parseContainerTaskToolResult(t *testing.T, result string) ChatToolResult {
	t.Helper()

	var r ChatToolResult
	if err := json.Unmarshal([]byte(result), &r); err != nil {
		t.Fatalf("failed to parse result: %v", err)
	}
	return r
}

func expectContainerTaskSuccess(t *testing.T, result string) {
	t.Helper()

	r := parseContainerTaskToolResult(t, result)
	if !r.Success {
		t.Errorf("expected success, got error: %s", r.Error)
	}
}

func expectContainerTaskCreated(t *testing.T, result string) {
	t.Helper()

	r := parseContainerTaskToolResult(t, result)
	if !r.Success {
		t.Errorf("expected success, got error: %s", r.Error)
	}
	data := r.Data.(map[string]any)
	if data[nameField] != testContainerTaskGeneratedName {
		t.Errorf("name = %v, want container-task-1", data[nameField])
	}
	if data[namespaceField] != defaultNamespace {
		t.Errorf("namespace = %v, want default", data[namespaceField])
	}
	if data[phaseField] != taskPhasePendingString {
		t.Errorf("phase = %v, want Pending", data[phaseField])
	}
}

func expectContainerTaskScheduled(t *testing.T, result string) {
	t.Helper()

	r := parseContainerTaskToolResult(t, result)
	if !r.Success {
		t.Errorf("expected success, got error: %s", r.Error)
	}
	data := r.Data.(map[string]any)
	msg := data[messageField].(string)
	if msg == "Task created" {
		t.Error("expected scheduled message, got one-time message")
	}
}

func expectContainerTaskError(t *testing.T, result, errorType string) {
	t.Helper()

	r := parseContainerTaskToolResult(t, result)
	if r.Success {
		t.Fatalf("expected failure with %s", errorType)
	}
	if r.ErrorType != errorType {
		t.Errorf("errorType = %v, want %s", r.ErrorType, errorType)
	}
}

func expectContainerTaskWorkspace(t *testing.T, fc client.Client) {
	t.Helper()

	task := &corev1alpha1.Task{}
	key := client.ObjectKey{Name: testContainerTaskGeneratedName, Namespace: defaultNamespace}
	if err := fc.Get(context.Background(), key, task); err != nil {
		t.Fatalf("failed to get created task: %v", err)
	}
	if task.Spec.Workspace == nil {
		t.Fatal("expected workspace")
	}
	if task.Spec.Workspace.GitRepo != "https://github.com/example/repo.git" {
		t.Errorf("gitRepo = %q", task.Spec.Workspace.GitRepo)
	}
	if task.Spec.Workspace.Ref != "abc123" {
		t.Errorf("ref = %q", task.Spec.Workspace.Ref)
	}
	if task.Spec.Workspace.GitSecretRef == nil || task.Spec.Workspace.GitSecretRef.Name != testGitCredentialsSecret {
		t.Fatalf("gitSecretRef = %#v", task.Spec.Workspace.GitSecretRef)
	}
	if task.Spec.PriorTaskRef == nil || task.Spec.PriorTaskRef.Name != testCoderTaskName {
		t.Fatalf("priorTaskRef = %#v", task.Spec.PriorTaskRef)
	}
	if task.Spec.PriorTaskRef.Namespace != defaultNamespace {
		t.Fatalf("priorTaskRef namespace = %q, want %q", task.Spec.PriorTaskRef.Namespace, defaultNamespace)
	}
}

func expectContainerTaskInheritedWorkspace(t *testing.T, fc client.Client) {
	t.Helper()

	task := &corev1alpha1.Task{}
	key := client.ObjectKey{Name: testContainerTaskGeneratedName, Namespace: defaultNamespace}
	if err := fc.Get(context.Background(), key, task); err != nil {
		t.Fatalf("failed to get created task: %v", err)
	}
	if task.Spec.Workspace == nil {
		t.Fatal("expected inherited workspace")
	}
	if task.Spec.Workspace.GitRepo != "https://github.com/example/prior.git" {
		t.Errorf("gitRepo = %q", task.Spec.Workspace.GitRepo)
	}
	if task.Spec.Workspace.Ref != "prior-sha" {
		t.Errorf("ref = %q", task.Spec.Workspace.Ref)
	}
	if task.Spec.Workspace.SubPath != "src" {
		t.Errorf("subPath = %q", task.Spec.Workspace.SubPath)
	}
	if task.Spec.Workspace.GitSecretRef == nil || task.Spec.Workspace.GitSecretRef.Name != testGitCredentialsSecret {
		t.Fatalf("gitSecretRef = %#v", task.Spec.Workspace.GitSecretRef)
	}
	if task.Spec.PriorTaskRef == nil || task.Spec.PriorTaskRef.Name != testCoderTaskName {
		t.Fatalf("priorTaskRef = %#v", task.Spec.PriorTaskRef)
	}
	if task.Spec.PriorTaskRef.Namespace != defaultNamespace {
		t.Fatalf("priorTaskRef namespace = %q, want %q", task.Spec.PriorTaskRef.Namespace, defaultNamespace)
	}
}

func TestCreateContainerTaskTool_Execute(t *testing.T) {
	tests := []struct {
		name        string
		args        json.RawMessage
		objects     []client.Object
		checkResult func(t *testing.T, result string)
		checkClient func(t *testing.T, fc client.Client)
	}{
		{
			name:        "happy path with image and command",
			args:        json.RawMessage(`{"name":"my-task","image":"alpine:latest","command":["sh","-c"],"args":["echo hello"]}`),
			checkResult: expectContainerTaskCreated,
		},
		{
			name:        "minimal args - name only",
			args:        json.RawMessage(`{"name":"min-task"}`),
			checkResult: expectContainerTaskSuccess,
		},
		{
			name:        "with priority and timeout",
			args:        json.RawMessage(`{"name":"my-task","image":"busybox","timeout":"5m","priority":100}`),
			checkResult: expectContainerTaskSuccess,
		},
		{
			name:        "with schedule",
			args:        json.RawMessage(`{"name":"cron-task","image":"busybox","schedule":"*/5 * * * *"}`),
			checkResult: expectContainerTaskScheduled,
		},
		{
			name: "with workspace",
			args: json.RawMessage(`{"name":"validation","image":"golang:1.26","command":["sh","-lc"],"args":["go test ./..."],"workspace":{"gitRepo":"https://github.com/example/repo.git","branch":"feature","ref":"abc123","gitSecretRef":"git-credentials","subPath":"src"},"prior_task":"coder-task"}`),
			objects: []client.Object{
				&corev1alpha1.Task{
					ObjectMeta: metav1.ObjectMeta{Name: testCoderTaskName, Namespace: defaultNamespace},
					Spec:       corev1alpha1.TaskSpec{Type: corev1alpha1.TaskTypeAgent},
				},
			},
			checkResult: expectContainerTaskSuccess,
			checkClient: expectContainerTaskWorkspace,
		},
		{
			name: "repo validation without workspace fails",
			args: json.RawMessage(`{"name":"validation","image":"golang:1.26","command":["sh","-lc"],"args":["go test ./..."]}`),
			checkResult: func(t *testing.T, result string) {
				expectContainerTaskError(t, result, "missing_workspace")
			},
		},
		{
			name: "repo validation inherits workspace from prior task",
			args: json.RawMessage(`{"name":"validation","image":"golang:1.26","command":["sh","-lc"],"args":["go test ./..."],"prior_task":"coder-task"}`),
			objects: []client.Object{
				&corev1alpha1.Task{
					ObjectMeta: metav1.ObjectMeta{Name: testCoderTaskName, Namespace: defaultNamespace},
					Spec: corev1alpha1.TaskSpec{
						Type: corev1alpha1.TaskTypeAgent,
						Workspace: &corev1alpha1.WorkspaceConfig{
							GitRepo:      "https://github.com/example/prior.git",
							Ref:          "prior-sha",
							SubPath:      "src",
							GitSecretRef: &corev1.LocalObjectReference{Name: testGitCredentialsSecret},
						},
					},
				},
			},
			checkResult: expectContainerTaskSuccess,
			checkClient: expectContainerTaskInheritedWorkspace,
		},
		{
			name: invalidJSONArgsCaseName,
			args: json.RawMessage(`{bad json`),
			checkResult: func(t *testing.T, result string) {
				expectContainerTaskError(t, result, errTypeInvalidArgs)
			},
		},
		{
			name: invalidTimeoutCaseName,
			args: json.RawMessage(`{"name":"t","timeout":"xyz"}`),
			checkResult: func(t *testing.T, result string) {
				expectContainerTaskError(t, result, errTypeInvalidArgs)
			},
		},
		{
			name: k8sAlreadyExistsErrorCaseName,
			args: json.RawMessage(`{"name":"existing"}`),
			objects: []client.Object{
				&corev1alpha1.Task{
					ObjectMeta: metav1.ObjectMeta{
						Name:      testContainerTaskGeneratedName,
						Namespace: defaultNamespace,
					},
					Spec: corev1alpha1.TaskSpec{Type: corev1alpha1.TaskTypeContainer},
				},
			},
			checkResult: func(t *testing.T, result string) {
				expectContainerTaskError(t, result, "already_exists")
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fc := newFakeClient(tt.objects...)
			ctx := newCreateContainerTaskToolCtx(fc)
			tool := &CreateContainerTaskTool{}

			result, err := tool.Execute(ctx, tt.args)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if tt.checkResult != nil {
				tt.checkResult(t, result)
			}
			if tt.checkClient != nil {
				tt.checkClient(t, fc)
			}
		})
	}
}

func TestCreateContainerTaskTool_ExecuteCoordination_InheritsParentProvenance(t *testing.T) {
	t.Setenv(envOrkaTaskName, parentTaskName)
	t.Setenv(envOrkaTaskNamespace, defaultNamespace)

	fc := newFakeClient(parentTask())
	tool := NewCreateContainerTaskTool(fc)
	result, err := tool.Execute(context.Background(), json.RawMessage(`{"image":"busybox","command":["echo"],"args":["hello"]}`))
	if err != nil {
		t.Fatalf("Execute() error = %v", err)
	}
	expectContainerTaskSuccess(t, result)

	taskList := &corev1alpha1.TaskList{}
	if err := fc.List(context.Background(), taskList); err != nil {
		t.Fatalf("failed to list tasks: %v", err)
	}
	var child *corev1alpha1.Task
	for i := range taskList.Items {
		if taskList.Items[i].Name != parentTaskName {
			child = &taskList.Items[i]
			break
		}
	}
	if child == nil {
		t.Fatal("child task not found")
	}
	expectInheritedTaskProvenance(t, child)
}

func TestCreateContainerTaskTool_Execute_MissingContext(t *testing.T) {
	tool := &CreateContainerTaskTool{}
	result, err := tool.Execute(context.Background(), json.RawMessage(`{"name":"t"}`))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	var r ChatToolResult
	if err := json.Unmarshal([]byte(result), &r); err != nil {
		t.Fatalf("failed to parse result: %v", err)
	}
	if r.Success {
		t.Error("expected failure for missing context")
	}
	if r.ErrorType != internalErrorType {
		t.Errorf("errorType = %v, want internal_error", r.ErrorType)
	}
}
