package debugbundle

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

const testBundleRunID = "run-42-00000000000000000000000000000001"

func TestAllocateCreatesRestrictiveSummaryAndTrace(t *testing.T) {
	parent := t.TempDir()
	fixed := time.Unix(0, 42).UTC()
	bundle, err := Allocate(parent, testBundleRunID, fixed)
	if err != nil {
		t.Fatal(err)
	}
	if bundle.RunID() != testBundleRunID || bundle.CreatedAt() != fixed || bundle.SummaryRoot() != filepath.Join(bundle.Path(), "summary") || bundle.TraceRoot() != filepath.Join(bundle.Path(), "trace") {
		t.Fatalf("bundle=%#v", bundle)
	}
	for _, path := range []string{bundle.Path(), bundle.SummaryRoot(), bundle.TraceRoot()} {
		info, err := os.Stat(path)
		if err != nil {
			t.Fatal(err)
		}
		if info.Mode().Perm() != 0o700 {
			t.Fatalf("%s mode=%#o", path, info.Mode().Perm())
		}
	}
	if err := bundle.Summary().WriteError("failed"); err != nil {
		t.Fatal(err)
	}
	info, err := os.Stat(filepath.Join(bundle.SummaryRoot(), ArtifactErrorLog))
	if err != nil {
		t.Fatal(err)
	}
	if info.Mode().Perm() != 0o600 {
		t.Fatalf("file mode=%#o", info.Mode().Perm())
	}
}
func TestAllocateRejectsExistingBundleWithoutChangingIt(t *testing.T) {
	parent := t.TempDir()
	bundlePath := filepath.Join(parent, testBundleRunID)
	if err := os.Mkdir(bundlePath, 0o700); err != nil {
		t.Fatal(err)
	}
	sentinelPath := filepath.Join(bundlePath, "sentinel")
	sentinel := []byte("existing bundle")
	if err := os.WriteFile(sentinelPath, sentinel, 0o600); err != nil {
		t.Fatal(err)
	}

	if _, err := Allocate(parent, testBundleRunID, time.Unix(0, 42)); err == nil || !strings.Contains(err.Error(), "already exists") {
		t.Fatalf("Allocate() error = %v, want collision", err)
	}
	if got, err := os.ReadFile(sentinelPath); err != nil || !bytes.Equal(got, sentinel) {
		t.Fatalf("sentinel = %q, %v", got, err)
	}
}

func TestAllocateRejectsUnsafeRunIDsBeforeCreatingParent(t *testing.T) {
	for _, runID := range []string{"", ".", "..", "../escape", `..\\escape`, "/absolute", " trailing "} {
		t.Run(runID, func(t *testing.T) {
			parent := filepath.Join(t.TempDir(), "debug")
			if _, err := Allocate(parent, runID, time.Time{}); err == nil {
				t.Fatalf("Allocate(%q) succeeded", runID)
			}
			if _, err := os.Stat(parent); !os.IsNotExist(err) {
				t.Fatalf("debug parent exists or stat failed: %v", err)
			}
		})
	}
}

func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) {
	bundle, err := Allocate(t.TempDir(), testBundleRunID, time.Unix(0, 42))
	if err != nil {
		t.Fatal(err)
	}
	summary := bundle.Summary()
	if err := summary.WriteInvocation(Invocation{Operation: "run"}); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteRedactedEffectiveConfig(testRedactedSummaryPayload{}); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteResolvedPipeline(testRedactedResolvedPipelinePayload{}); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteResolvedReferences(nil); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteCheckpointEvents(nil); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteRunManifest(artifacts.RunManifest{RunID: bundle.RunID()}); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteChunkPlan(artifacts.ChunkPlanSummary{}); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteRunReport(RunReport{RunID: bundle.RunID(), PipelineID: "test"}); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteDiagnostics(contracts.DiagnosticCollection{}); err != nil {
		t.Fatal(err)
	}
	if err := summary.WriteError("failed"); err != nil {
		t.Fatal(err)
	}

	for _, name := range []string{
		ArtifactInvocationMetadata,
		ArtifactEffectiveConfig,
		ArtifactResolvedPipeline,
		ArtifactResolvedReferences,
		ArtifactCheckpointEvents,
		ArtifactRunManifest,
		ArtifactChunkPlan,
		ArtifactRunReport,
		ArtifactDiagnostics,
		ArtifactErrorLog,
	} {
		info, err := os.Stat(filepath.Join(bundle.SummaryRoot(), name))
		if err != nil {
			t.Fatalf("summary artifact %q: %v", name, err)
		}
		if info.Mode().Perm() != 0o600 {
			t.Fatalf("summary artifact %q mode=%#o", name, info.Mode().Perm())
		}
	}
}

func TestWriteInvocationPreservesReasoningEffortOverrideStates(t *testing.T) {
	replacement := "focused"
	cleared := ""
	tests := []struct {
		name      string
		override  *string
		wantValue string
		wantSet   bool
	}{
		{name: "inherit"},
		{name: "replace", override: &replacement, wantValue: "focused", wantSet: true},
		{name: "clear", override: &cleared, wantSet: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			bundle, err := Allocate(t.TempDir(), testBundleRunID, time.Unix(0, 42))
			if err != nil {
				t.Fatal(err)
			}
			if err := bundle.Summary().WriteInvocation(Invocation{
				Operation:               "run",
				ReasoningEffortOverride: tt.override,
			}); err != nil {
				t.Fatal(err)
			}
			data, err := os.ReadFile(filepath.Join(bundle.SummaryRoot(), ArtifactInvocationMetadata))
			if err != nil {
				t.Fatal(err)
			}
			var payload map[string]any
			if err := json.Unmarshal(data, &payload); err != nil {
				t.Fatal(err)
			}
			value, found := payload["reasoning_effort_override"]
			if found != tt.wantSet || (found && value != tt.wantValue) {
				t.Fatalf("reasoning override found=%t value=%#v, want found=%t value=%q; JSON=%s", found, value, tt.wantSet, tt.wantValue, data)
			}
		})
	}
}

func TestWriteInvocationOmitsEmptySessionID(t *testing.T) {
	for _, sessionID := range []string{"", "session-123"} {
		bundle, err := Allocate(t.TempDir(), testBundleRunID, time.Unix(0, 42))
		if err != nil {
			t.Fatal(err)
		}
		if err := bundle.Summary().WriteInvocation(Invocation{Operation: "run", SessionID: sessionID}); err != nil {
			t.Fatal(err)
		}
		data, err := os.ReadFile(filepath.Join(bundle.SummaryRoot(), ArtifactInvocationMetadata))
		if err != nil {
			t.Fatal(err)
		}
		var payload map[string]any
		if err := json.Unmarshal(data, &payload); err != nil {
			t.Fatal(err)
		}
		value, found := payload["session_id"]
		if found != (sessionID != "") || (found && value != sessionID) {
			t.Fatalf("session found=%t value=%#v, want found=%t value=%q; JSON=%s", found, value, sessionID != "", sessionID, data)
		}
	}
}
func TestSummaryWriterInternalWritesConfineArtifacts(t *testing.T) {
	bundle, err := Allocate(t.TempDir(), testBundleRunID, time.Unix(0, 42))
	if err != nil {
		t.Fatal(err)
	}
	if err := bundle.Summary().writeJSON("../outside.json", map[string]any{}); err == nil {
		t.Fatal("accepted traversal")
	}
	if err := bundle.Summary().writeBytes(`trace\\x`, []byte("x")); err == nil {
		t.Fatal("accepted backslash")
	}
}

type testRedactedSummaryPayload struct{}

func (testRedactedSummaryPayload) RedactedSummaryPayload() any {
	return map[string]any{"redacted": true}
}

type testRedactedResolvedPipelinePayload struct{}

func (testRedactedResolvedPipelinePayload) RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline {
	return pipeline.ResolvedPipeline{ID: "redacted"}
}
