package pipeline

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

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

func TestCleanDebugPathPreservesRawComponents(t *testing.T) {
	for _, test := range []struct {
		value string
		want  string
	}{
		{value: "", want: "%"},
		{value: ".", want: "%2E"},
		{value: "..", want: "%2E%2E"},
		{value: "a//b", want: "a/%/b"},
		{value: "/a/", want: "%/a/%"},
		{value: " a ", want: "%20a%20"},
	} {
		if got := cleanDebugPath(test.value); got != test.want {
			t.Errorf("cleanDebugPath(%q) = %q, want %q", test.value, got, test.want)
		}
	}
}

func TestDebugLLMPathsKeepDotIdentitiesDistinct(t *testing.T) {
	recorder := newCapturedDebugRecorder()
	client := WithDebugLLMRecording(attemptDebugLLM{}, recorder)
	for _, test := range []struct {
		stageName string
		path      string
	}{
		{stageName: ".", path: "%2E/response-0001.json"},
		{stageName: "..", path: "%2E%2E/response-0002.json"},
	} {
		if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{StageName: test.stageName}, nil); err != nil {
			t.Fatalf("CompleteStructured(%q): %v", test.stageName, err)
		}
		if !recorder.has(test.path) {
			t.Errorf("debug artifact %q was not written; names = %#v", test.path, recorder.names())
		}
	}
}

func TestDebugCompletionRequestOmitsCorrectionContent(t *testing.T) {
	const assistantResponse = `{"secret":"assistant response"}`
	const userGuidance = "secret user guidance"
	correction, err := contracts.NewSemanticCorrection([]byte(assistantResponse), userGuidance)
	if err != nil {
		t.Fatalf("NewSemanticCorrection() error = %v", err)
	}
	summary := debugCompletionRequest(contracts.StructuredCompletionRequest{
		Inputs:     contracts.LLMInputSet{"source": contracts.NewLLMInputMaterial("source", "application/json", []byte(`{"source":true}`), "", "")},
		Vars:       map[string]any{"custom": "value"},
		Correction: correction,
	})
	encoded, err := json.Marshal(summary)
	if err != nil {
		t.Fatalf("marshal completion summary: %v", err)
	}
	for _, secret := range []string{assistantResponse, userGuidance} {
		if strings.Contains(string(encoded), secret) {
			t.Fatalf("debug completion summary leaked %q: %s", secret, encoded)
		}
	}
	if summary.InputCount != 1 || summary.VariableCount != 1 || summary.Correction == nil {
		t.Fatalf("debug completion summary = %#v, want counts and correction metadata", summary)
	}
}

func TestDebugSourceDocumentPreservesUnitReferences(t *testing.T) {
	doc := validSourceDocument()
	envelope := debugSourceDocumentEnvelope(doc)
	encoded, err := json.Marshal(envelope)
	if err != nil {
		t.Fatalf("Marshal(debug source document) error = %v, want nil", err)
	}

	var decoded debugSourceDocument
	if err := json.Unmarshal(encoded, &decoded); err != nil {
		t.Fatalf("Unmarshal(debug source document) error = %v, want nil", err)
	}
	if got, want := decoded.Units[0].Ref, (source.SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 1}); got != want {
		t.Fatalf("debug source unit ref = %#v, want %#v", got, want)
	}

	envelope.Units[0].Ref.SourceID = "mutated"
	if got := doc.Units[0].Ref.SourceID; got != "source-1" {
		t.Fatalf("source document ref = %q after debug mutation, want source-1", got)
	}
}

func TestDebugSourceUnitsRedactSecrets(t *testing.T) {
	units := []source.SourceUnit{{
		ID:       1,
		Kind:     "paragraph",
		Text:     "application text Bearer secretvalue sk-secretvalue",
		Ref:      source.SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 1},
		Metadata: map[string]any{"api_key": "sk-secretvalue"},
	}}

	got := cloneSourceUnitsForDebug(units)
	if len(got) != 1 {
		t.Fatalf("debug unit count = %d, want 1", len(got))
	}
	if !strings.Contains(got[0].Text, "application text") {
		t.Fatalf("debug unit text = %q, want application content retained", got[0].Text)
	}
	for _, forbidden := range []string{"secretvalue", "sk-secretvalue"} {
		if strings.Contains(got[0].Text, forbidden) {
			t.Fatalf("debug unit text contains %q: %q", forbidden, got[0].Text)
		}
	}
	if got, want := got[0].Metadata["api_key"], "[REDACTED]"; got != want {
		t.Fatalf("debug unit metadata api_key = %#v, want %q", got, want)
	}
	if units[0].Text != "application text Bearer secretvalue sk-secretvalue" {
		t.Fatalf("source unit text was mutated: %q", units[0].Text)
	}
}

func TestDebugSourceChunkPreservesReference(t *testing.T) {
	doc := validSourceDocument()
	chunk := source.Chunk{
		ID: "chunk-1", SourceID: doc.ID, Index: 0,
		Ref:     source.SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 1},
		Content: []byte("chunk content"), MediaType: "text/plain", Units: doc.Units[:1],
	}
	envelope := debugSourceChunkEnvelope(chunk)
	encoded, err := json.Marshal(envelope)
	if err != nil {
		t.Fatalf("Marshal(debug source chunk) error = %v, want nil", err)
	}
	var decoded debugSourceChunk
	if err := json.Unmarshal(encoded, &decoded); err != nil {
		t.Fatalf("Unmarshal(debug source chunk) error = %v, want nil", err)
	}
	if decoded.Ref != chunk.Ref {
		t.Fatalf("debug chunk ref = %#v, want %#v", decoded.Ref, chunk.Ref)
	}
}
