package workspace

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"testing"

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

	iop "iop/proto/gen/iop"
)

func TestWorkspaceObservation(t *testing.T) {
	root := t.TempDir()
	core, logs := observer.New(zap.InfoLevel)
	runtime, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", zap.New(core))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = runtime.Close() })
	if _, err := runtime.Open(testRequestAuthority("request-observation")); err != nil {
		t.Fatal(err)
	}
	const sentinel = "SECRET_PATH_COMMAND_OUTPUT_BEARER"
	if result := runtime.Write("request-observation", sentinel, []byte(sentinel)); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("write result = %+v", result)
	}
	cleanup := runtime.Cleanup(t.Context(), "request-observation")
	if cleanup.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("cleanup result = %+v", cleanup)
	}

	allowed := map[string]bool{
		"correlation": true, "event": true, "operation": true, "outcome": true, "error_code": true,
		"duration_ms": true, "truncated": true, "process_count": true, "artifact_count": true,
	}
	entries := logs.All()
	if len(entries) != 2 {
		t.Fatalf("workspace logs = %d, want 2", len(entries))
	}
	for _, entry := range entries {
		if entry.Message != workspaceObservationLogKey {
			t.Fatalf("log message = %q", entry.Message)
		}
		if strings.Contains(strings.ToLower(fmt.Sprint(entry.ContextMap()["correlation"])), "secret") {
			t.Fatalf("secret correlation leaked: %+v", entry)
		}
		if len(entry.Context) != len(allowed) {
			t.Fatalf("log field count = %d, want %d", len(entry.Context), len(allowed))
		}
		for _, field := range entry.Context {
			if !allowed[field.Key] {
				t.Fatalf("unexpected log key %q", field.Key)
			}
			if strings.Contains(strings.ToLower(field.String), "secret") {
				t.Fatalf("secret sentinel leaked in %q", field.Key)
			}
		}
	}
}

func TestWorkspaceObservationFailureIsolation(t *testing.T) {
	runtime, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(t.TempDir())}, "darwin", zap.NewNop())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = runtime.Close() })
	if _, err := runtime.Open(testRequestAuthority("request-failure-isolation")); err != nil {
		t.Fatal(err)
	}
	runtime.observer = &workspaceSafeObserver{inner: workspaceObserverFunc(func(workspaceObservation) error {
		panic("observer panic")
	})}
	if result := runtime.Write("request-failure-isolation", "result.txt", []byte("kept")); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("write changed by observer panic: %+v", result)
	}
	if cleanup := runtime.Cleanup(t.Context(), "request-failure-isolation"); cleanup.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("cleanup changed by observer panic: %+v", cleanup)
	}
	if got := runtime.observer.failureCount(); got != 2 {
		t.Fatalf("isolated panic failures = %d, want 2", got)
	}

	runtime.observer = &workspaceSafeObserver{inner: workspaceObserverFunc(func(workspaceObservation) error { return errors.New("observer error") })}
	if result := runtime.Write("unknown-request", "result.txt", nil); result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("expected normal invalid result, got %+v", result)
	}
	if got := runtime.observer.failureCount(); got != 1 {
		t.Fatalf("isolated error failures = %d, want 1", got)
	}
}

type workspaceObserverFunc func(workspaceObservation) error

func (fn workspaceObserverFunc) Emit(observation workspaceObservation) error { return fn(observation) }

type capturingObserver struct {
	mu           sync.Mutex
	observations []workspaceObservation
}

func (c *capturingObserver) Emit(obs workspaceObservation) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.observations = append(c.observations, obs)
	return nil
}

func TestWorkspaceObservationCorrelationSurvivesCleanupOverlap(t *testing.T) {
	root := t.TempDir()
	runtime, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", zap.NewNop())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = runtime.Close() })
	if _, err := runtime.Open(testRequestAuthority("request-cleanup-overlap")); err != nil {
		t.Fatal(err)
	}
	request, err := runtime.Request("request-cleanup-overlap")
	if err != nil {
		t.Fatal(err)
	}
	expectedCorrelation := request.correlation
	if expectedCorrelation == "" {
		t.Fatalf("expected non-empty correlation at open, got empty")
	}
	capturer := &capturingObserver{}
	runtime.observer = &workspaceSafeObserver{inner: capturer}
	entered := make(chan struct{})
	release := make(chan struct{})
	request.entry.beforeRename = func() error {
		close(entered)
		<-release
		return nil
	}
	writeDone := make(chan Result, 1)
	go func() {
		writeDone <- runtime.Write("request-cleanup-overlap", "result.txt", []byte("overlap"))
	}()
	<-entered
	cleanup := runtime.Cleanup(t.Context(), "request-cleanup-overlap")
	if cleanup.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("cleanup result = %+v, want success", cleanup)
	}
	close(release)
	writeResult := <-writeDone
	if writeResult.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
		t.Fatalf("write after cleanup overlap = %+v, want success", writeResult)
	}

	capturer.mu.Lock()
	defer capturer.mu.Unlock()
	if len(capturer.observations) != 2 {
		t.Fatalf("observations count = %d, want 2 (1 cleanup + 1 tool)", len(capturer.observations))
	}
	var cleanupCount, toolCount int
	var cleanupCorrelation, toolCorrelation string
	for _, obs := range capturer.observations {
		switch obs.event {
		case workspaceObservationCleanup:
			cleanupCount++
			cleanupCorrelation = obs.correlation
		case workspaceObservationTool:
			toolCount++
			toolCorrelation = obs.correlation
		}
	}
	if cleanupCount != 1 {
		t.Fatalf("cleanup observation count = %d, want 1", cleanupCount)
	}
	if toolCount != 1 {
		t.Fatalf("tool observation count = %d, want 1", toolCount)
	}
	if cleanupCorrelation == "" {
		t.Fatalf("cleanup correlation is empty")
	}
	if toolCorrelation == "" {
		t.Fatalf("tool correlation is empty after cleanup overlap")
	}
	if cleanupCorrelation != toolCorrelation {
		t.Fatalf("cleanup correlation=%q != tool correlation=%q, want shared correlation", cleanupCorrelation, toolCorrelation)
	}
	if cleanupCorrelation != expectedCorrelation {
		t.Fatalf("correlation=%q != expected=%q, want immutable request-local correlation", cleanupCorrelation, expectedCorrelation)
	}
}
