package service

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"
	"time"

	edgenode "iop/apps/edge/internal/node"
	"iop/packages/go/config"
)

type recordingWorkspaceExecutor struct {
	calls atomic.Int32
	seen  chan *SingleRequestBinding
}

func (e *recordingWorkspaceExecutor) ExecuteSingleRequest(_ context.Context, req SingleRequestRequest, _ SingleRequestController) error {
	e.calls.Add(1)
	e.seen <- req.Binding.Clone()
	return nil
}

func (e *recordingWorkspaceExecutor) binding(t *testing.T) *SingleRequestBinding {
	t.Helper()
	select {
	case binding := <-e.seen:
		return binding
	case <-time.After(time.Second):
		t.Fatal("executor did not receive an admitted binding")
		return nil
	}
}

func newRecordingWorkspaceExecutor() *recordingWorkspaceExecutor {
	return &recordingWorkspaceExecutor{seen: make(chan *SingleRequestBinding, 1)}
}

func workspaceDefinition(ref string, operations ...config.WorkspaceOperation) config.WorkspaceDefinition {
	workspace := config.WorkspaceDefinition{
		Ref:                 ref,
		Platform:            "darwin",
		Root:                "/Users/operator/project",
		Operations:          operations,
		MaxReadBytes:        64,
		MaxWriteBytes:       64,
		MaxOutputBytes:      128,
		MaxCommandTimeoutMS: 500,
	}
	for _, operation := range operations {
		if operation == config.WorkspaceOpCommand {
			workspace.Commands = []config.WorkspaceCommandDefinition{{ID: "test"}}
		}
	}
	return workspace
}

func workspaceStore(nodeID string, workspace config.WorkspaceDefinition) *edgenode.NodeStore {
	store := edgenode.NewNodeStore()
	store.Add(&edgenode.NodeRecord{ID: nodeID, Alias: nodeID, Token: nodeID + "-token", Workspaces: []config.WorkspaceDefinition{workspace}})
	return store
}

func readyWorkspaceService(t *testing.T, nodeID string, workspace config.WorkspaceDefinition, executor *recordingWorkspaceExecutor) (*Service, *edgenode.Registry) {
	t.Helper()
	registry := edgenode.NewRegistry()
	registry.Register(&edgenode.NodeEntry{NodeID: nodeID, Alias: nodeID})
	service := New(registry, nil)
	service.SetNodeStore(workspaceStore(nodeID, workspace))
	service.SetSingleRequestExecutor(executor)
	return service, registry
}

func workspaceRequest(t *testing.T, ref string) SingleRequestRequest {
	t.Helper()
	binding := createTestBinding(t)
	binding.WorkspaceRef = ref
	return SingleRequestRequest{RequestID: "workspace-request", Binding: binding, Prompt: "complete task"}
}

func TestSingleRequestWorkspaceRejectsMissingRuntimeDependencies(t *testing.T) {
	for name, service := range map[string]*Service{
		"missing store and registry": {},
		"missing store":              New(edgenode.NewRegistry(), nil),
		"missing registry":           &Service{nodeStore: workspaceStore("node-a", workspaceDefinition("approved", config.WorkspaceOpRead))},
	} {
		t.Run(name, func(t *testing.T) {
			executor := newRecordingWorkspaceExecutor()
			service.SetSingleRequestExecutor(executor)
			request := workspaceRequest(t, "approved")
			_, err := service.StartSingleRequest(context.Background(), request)
			if !errors.Is(err, ErrSingleRequestWorkspaceUnavailable) {
				t.Fatalf("StartSingleRequest error = %v, want workspace unavailable", err)
			}
			if got := executor.calls.Load(); got != 0 {
				t.Fatalf("executor calls = %d, want 0", got)
			}
		})
	}
}

func TestSingleRequestWorkspaceOperationSpecificLimits(t *testing.T) {
	tests := []struct {
		name       string
		operations []config.WorkspaceOperation
		limits     SingleRequestWorkspaceLimits
		commands   []string
		valid      bool
	}{
		{"read", []config.WorkspaceOperation{config.WorkspaceOpRead}, SingleRequestWorkspaceLimits{MaxReadBytes: 1}, nil, true},
		{"write", []config.WorkspaceOperation{config.WorkspaceOpWrite}, SingleRequestWorkspaceLimits{MaxWriteBytes: 1}, nil, true},
		{"list", []config.WorkspaceOperation{config.WorkspaceOpList}, SingleRequestWorkspaceLimits{MaxOutputBytes: 1}, nil, true},
		{"delete", []config.WorkspaceOperation{config.WorkspaceOpDelete}, SingleRequestWorkspaceLimits{}, nil, true},
		{"command", []config.WorkspaceOperation{config.WorkspaceOpCommand}, SingleRequestWorkspaceLimits{MaxOutputBytes: 1, MaxCommandTimeoutMS: 1}, []string{"command"}, true},
		{"read requires bound", []config.WorkspaceOperation{config.WorkspaceOpRead}, SingleRequestWorkspaceLimits{}, nil, false},
		{"list requires output", []config.WorkspaceOperation{config.WorkspaceOpList}, SingleRequestWorkspaceLimits{}, nil, false},
		{"command requires id", []config.WorkspaceOperation{config.WorkspaceOpCommand}, SingleRequestWorkspaceLimits{MaxOutputBytes: 1, MaxCommandTimeoutMS: 1}, nil, false},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			workspace := workspaceDefinition("approved", test.operations...)
			workspace.MaxReadBytes = test.limits.MaxReadBytes
			workspace.MaxWriteBytes = test.limits.MaxWriteBytes
			workspace.MaxOutputBytes = test.limits.MaxOutputBytes
			workspace.MaxCommandTimeoutMS = test.limits.MaxCommandTimeoutMS
			workspace.Commands = make([]config.WorkspaceCommandDefinition, len(test.commands))
			for i, id := range test.commands {
				workspace.Commands[i] = config.WorkspaceCommandDefinition{ID: id}
			}
			_, err := compileSingleRequestWorkspaceBinding(workspace, &edgenode.NodeEntry{NodeID: "node-a", ConnectionGeneration: 1})
			if test.valid && err != nil {
				t.Fatalf("compileSingleRequestWorkspaceBinding: %v", err)
			}
			if !test.valid && !errors.Is(err, errSingleRequestWorkspaceMalformed) {
				t.Fatalf("compileSingleRequestWorkspaceBinding error = %v, want malformed", err)
			}
		})
	}
}

func TestSingleRequestWorkspaceAdmissionMatrix(t *testing.T) {
	approved := workspaceDefinition("approved", config.WorkspaceOpRead)
	for _, test := range []struct {
		name  string
		setup func(*edgenode.Registry) *edgenode.NodeStore
		ref   string
	}{
		{
			name: "unapproved ref",
			setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
				registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
				return workspaceStore("node-a", approved)
			},
			ref: "unapproved",
		},
		{
			name: "foreign ready node is not a fallback",
			setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
				registry.Register(&edgenode.NodeEntry{NodeID: "node-b"})
				return workspaceStore("node-a", approved)
			},
			ref: "approved",
		},
		{
			name: "configured owner is pending",
			setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
				registry.RegisterIfAbsent(&edgenode.NodeEntry{NodeID: "node-a"})
				registry.Register(&edgenode.NodeEntry{NodeID: "node-b"})
				return workspaceStore("node-a", approved)
			},
			ref: "approved",
		},
		{
			name: "malformed empty workspace operations",
			setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
				registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
				return workspaceStore("node-a", workspaceDefinition("approved"))
			},
			ref: "approved",
		},
		{
			name: "duplicate workspace operation",
			setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
				registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
				workspace := workspaceDefinition("approved", config.WorkspaceOpRead, config.WorkspaceOpRead)
				return workspaceStore("node-a", workspace)
			},
			ref: "approved",
		},
		{
			name: "unsupported workspace operation",
			setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
				registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
				workspace := workspaceDefinition("approved", config.WorkspaceOperation("unsupported"))
				return workspaceStore("node-a", workspace)
			},
			ref: "approved",
		},
		{
			name: "command without command operation",
			setup: func(registry *edgenode.Registry) *edgenode.NodeStore {
				registry.Register(&edgenode.NodeEntry{NodeID: "node-a"})
				workspace := workspaceDefinition("approved", config.WorkspaceOpRead)
				workspace.Commands = []config.WorkspaceCommandDefinition{{ID: "test"}}
				return workspaceStore("node-a", workspace)
			},
			ref: "approved",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			registry := edgenode.NewRegistry()
			executor := newRecordingWorkspaceExecutor()
			service := New(registry, nil)
			service.SetNodeStore(test.setup(registry))
			service.SetSingleRequestExecutor(executor)
			_, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, test.ref))
			if !errors.Is(err, ErrSingleRequestWorkspaceUnavailable) {
				t.Fatalf("StartSingleRequest error = %v, want unavailable", err)
			}
			if got := executor.calls.Load(); got != 0 {
				t.Fatalf("executor calls = %d, want 0", got)
			}
		})
	}
}

func TestSingleRequestWorkspaceFreezesOwnerAcrossRefreshAndReconnect(t *testing.T) {
	workspace := workspaceDefinition("approved", config.WorkspaceOpCommand)
	executor := newRecordingWorkspaceExecutor()
	service, registry := readyWorkspaceService(t, "node-a", workspace, executor)
	handoff := make(chan struct{})
	refreshDone := make(chan struct{})
	actorDone := make(chan struct{})
	service.beforeSingleRequestHandoff = func() {
		close(handoff)
		<-refreshDone
	}
	go func() {
		defer close(actorDone)
		<-handoff
		service.SetNodeStore(workspaceStore("node-b", workspaceDefinition("approved", config.WorkspaceOpRead)))
		close(refreshDone)
	}()
	if _, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, "approved")); err != nil {
		t.Fatalf("StartSingleRequest: %v", err)
	}
	<-actorDone
	bound := executor.binding(t)
	if bound.Workspace.NodeID != "node-a" || bound.Workspace.ConnectionGeneration == 0 {
		t.Fatalf("executor binding = %#v, want original ready owner", bound.Workspace)
	}
	if len(bound.Workspace.CommandIDs) != 1 || bound.Workspace.CommandIDs[0] != "test" {
		t.Fatalf("executor binding lost frozen command capability: %#v", bound.Workspace)
	}
	if !registry.IsCurrentOwnerGeneration("node-a", bound.Workspace.ConnectionGeneration) {
		t.Fatal("refresh retargeted the admitted owner")
	}
}

func TestSingleRequestWorkspaceRejectsStaleGenerationBeforeExecutor(t *testing.T) {
	workspace := workspaceDefinition("approved", config.WorkspaceOpRead)
	executor := newRecordingWorkspaceExecutor()
	service, registry := readyWorkspaceService(t, "node-a", workspace, executor)
	handoff := make(chan struct{})
	reconnectDone := make(chan struct{})
	actorDone := make(chan struct{})
	service.beforeSingleRequestHandoff = func() {
		close(handoff)
		<-reconnectDone
	}
	go func() {
		defer close(actorDone)
		<-handoff
		registry.Unregister("node-a")
		registry.Register(&edgenode.NodeEntry{NodeID: "node-a", Alias: "node-a"})
		close(reconnectDone)
	}()
	_, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, "approved"))
	<-actorDone
	if !errors.Is(err, ErrSingleRequestWorkspaceStale) {
		t.Fatalf("StartSingleRequest error = %v, want stale workspace", err)
	}
	if got := executor.calls.Load(); got != 0 {
		t.Fatalf("executor calls = %d, want 0", got)
	}
}

func TestSingleRequestWorkspaceAppliesPresetMinima(t *testing.T) {
	workspace := workspaceDefinition("approved", config.WorkspaceOpCommand)
	preset := createTestBinding(t).Limits
	workspace.MaxOutputBytes = preset.MaxOutputBytes + 1
	workspace.MaxCommandTimeoutMS = preset.StageTimeoutMS + 1
	executor := newRecordingWorkspaceExecutor()
	service, _ := readyWorkspaceService(t, "node-a", workspace, executor)
	if _, err := service.StartSingleRequest(context.Background(), workspaceRequest(t, "approved")); err != nil {
		t.Fatalf("StartSingleRequest: %v", err)
	}
	bound := executor.binding(t)
	if bound.Workspace.Limits.MaxOutputBytes != preset.MaxOutputBytes || bound.Workspace.Limits.MaxCommandTimeoutMS != preset.StageTimeoutMS {
		t.Fatalf("effective limits = %#v, want preset minima", bound.Workspace.Limits)
	}
}
