package bootstrap

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"

	"iop/apps/node/internal/transport"
	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

func TestWorkspaceRuntimeCompositionBeforeReady(t *testing.T) {
	for _, hostOS := range []string{config.WorkspacePlatformDarwin, config.WorkspacePlatformLinux} {
		t.Run(hostOS, func(t *testing.T) {
			t.Chdir(t.TempDir())
			root := t.TempDir()
			configPayload := &iop.NodeConfigPayload{
				Runtime: &iop.NodeRuntimeConfig{Concurrency: 1},
				Workspaces: []*iop.WorkspaceConfig{{
					Ref: "workspace-1", Platform: hostOS, Root: root,
					Operations:   []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ},
					MaxReadBytes: 8,
				}},
			}
			dialer := func(context.Context, string, string, *zap.Logger) (*transport.RegisterResult, error) {
				return &transport.RegisterResult{NodeID: "node-1", Config: configPayload}, nil
			}
			events := make([]string, 0, 2)
			handlerInstalled := false
			var installedWorkspaceHandler transport.WorkspaceHandler
			owner, err := connectRuntime(context.Background(), &config.NodeConfig{}, zap.NewNop(), nil, connectRuntimeOptions{
				dialer: dialer,
				hostOS: func() string { return hostOS },
				setHandler: func(_ *transport.Session, handler transport.Handler) {
					workspaceHandler, ok := handler.(transport.WorkspaceHandler)
					if !ok {
						t.Fatal("composed handler does not implement WorkspaceHandler")
					}
					installedWorkspaceHandler = workspaceHandler
					response, callErr := workspaceHandler.OnWorkspaceOpen(context.Background(), nil, &iop.WorkspaceOpenRequest{
						RequestId: "request-1", WorkspaceRef: "workspace-1",
						Operations:   []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ},
						MaxReadBytes: 8,
					})
					if callErr != nil || response.GetStatus() != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
						t.Fatalf("workspace handler before ready = %+v, %v", response, callErr)
					}
					handlerInstalled = true
					events = append(events, "handler")
				},
				signalReady: func(_ *transport.Session, _ time.Duration) error {
					if !handlerInstalled {
						t.Fatal("ready signalled before handler installation")
					}
					events = append(events, "ready")
					return nil
				},
			})
			if err != nil {
				t.Fatalf("connectRuntime: %v", err)
			}
			owner.close()
			closedResponse, callErr := installedWorkspaceHandler.OnWorkspaceOpen(context.Background(), nil, &iop.WorkspaceOpenRequest{
				RequestId: "request-2", WorkspaceRef: "workspace-1",
				Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}, MaxReadBytes: 8,
			})
			if callErr != nil || closedResponse.GetErrorCode() != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY {
				t.Fatalf("workspace runtime after owner close = %+v, %v", closedResponse, callErr)
			}
			if strings.Join(events, ",") != "handler,ready" {
				t.Fatalf("composition order = %v", events)
			}
		})
	}
}

func TestWorkspaceRuntimeCatalogFailureIsRedactedBeforeReady(t *testing.T) {
	t.Chdir(t.TempDir())
	const sentinel = "workspace-startup-root-sentinel-8403"
	core, observed := observer.New(zap.DebugLevel)
	logger := zap.New(core)
	readyReached := false
	_, err := connectRuntime(context.Background(), &config.NodeConfig{}, logger, nil, connectRuntimeOptions{
		dialer: func(context.Context, string, string, *zap.Logger) (*transport.RegisterResult, error) {
			return &transport.RegisterResult{NodeID: "node-1", Config: &iop.NodeConfigPayload{
				Runtime: &iop.NodeRuntimeConfig{Concurrency: 1},
				Workspaces: []*iop.WorkspaceConfig{{
					Ref: "workspace-1", Platform: "darwin", Root: t.TempDir() + "/" + sentinel,
					Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_READ}, MaxReadBytes: 1,
				}},
			}}, nil
		},
		hostOS: func() string { return "darwin" },
		setHandler: func(*transport.Session, transport.Handler) {
			t.Fatal("handler installed after invalid workspace catalog")
		},
		signalReady: func(*transport.Session, time.Duration) error {
			readyReached = true
			return nil
		},
	})
	if err == nil || readyReached {
		t.Fatalf("catalog error=%v ready=%v", err, readyReached)
	}
	if strings.Contains(err.Error(), sentinel) || strings.Contains(fmt.Sprint(observed.All()), sentinel) {
		t.Fatalf("workspace root leaked: error=%q logs=%v", err, observed.All())
	}
}

func TestWorkspaceRuntimeOwnerCloseOrderAndReconnectReplacement(t *testing.T) {
	var events []string
	owner := &runtimeOwner{
		closeRegistry:  func() { events = append(events, "registry") },
		closeWorkspace: func() { events = append(events, "workspace") },
		closeSession:   func() { events = append(events, "session") },
		closeStore:     func() { events = append(events, "store") },
	}
	owner.close()
	owner.close()
	if strings.Join(events, ",") != "registry,workspace,session,store" {
		t.Fatalf("direct close order = %v", events)
	}

	events = nil
	previous := &runtimeOwner{
		closeRegistry:  func() { events = append(events, "old-registry") },
		closeWorkspace: func() { events = append(events, "old-workspace") },
		closeSession:   func() { events = append(events, "old-session") },
		closeStore:     func() { events = append(events, "old-store") },
	}
	next := &runtimeOwner{
		closeRegistry:  func() { events = append(events, "new-registry") },
		closeWorkspace: func() { events = append(events, "new-workspace") },
		closeSession:   func() { events = append(events, "new-session") },
		closeStore:     func() { events = append(events, "new-store") },
	}
	supervisor := &runtimeSupervisor{current: previous}
	supervisor.swapOwner(next)
	supervisor.clearCurrent()
	want := "old-registry,old-workspace,old-session,old-store,new-registry,new-workspace,new-session,new-store"
	if strings.Join(events, ",") != want {
		t.Fatalf("replacement close order = %v", events)
	}
}
