package evidencecontext

import (
	"bytes"
	"encoding/json"
	"math"
	"os"
	"reflect"
	"strings"
	"testing"

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

func TestBuildExpandsAndMergesEvidenceByDocumentPosition(t *testing.T) {
	for _, test := range []struct {
		name      string
		window    int
		evidence  []LaneEvidence
		wantUnits [][]int
		wantRefs  [][]EvidenceRef
	}{
		{
			name:      "zero window",
			evidence:  []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}},
			wantUnits: [][]int{{3}},
			wantRefs:  [][]EvidenceRef{{{LaneID: "npcs", SourceRef: ref(3, 3)}}},
		},
		{
			name:      "non monotonic ids use positions and clip boundaries",
			window:    1,
			evidence:  []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}},
			wantUnits: [][]int{{10, 3, 30}},
			wantRefs:  [][]EvidenceRef{{{LaneID: "npcs", SourceRef: ref(3, 3)}}},
		},
		{
			name:      "separate gaps stay separate",
			evidence:  []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(10, 10), ref(50, 50)}}},
			wantUnits: [][]int{{10}, {50}},
			wantRefs:  [][]EvidenceRef{{{LaneID: "npcs", SourceRef: ref(10, 10)}}, {{LaneID: "npcs", SourceRef: ref(50, 50)}}},
		},
		{
			name:      "overlapping windows merge",
			window:    1,
			evidence:  []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3), ref(30, 30)}}},
			wantUnits: [][]int{{10, 3, 30, 7}},
			wantRefs:  [][]EvidenceRef{{{LaneID: "npcs", SourceRef: ref(3, 3)}, {LaneID: "npcs", SourceRef: ref(30, 30)}}},
		},
		{
			name:      "contiguous windows merge",
			window:    1,
			evidence:  []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(10, 10), ref(7, 7)}}},
			wantUnits: [][]int{{10, 3, 30, 7, 50}},
			wantRefs:  [][]EvidenceRef{{{LaneID: "npcs", SourceRef: ref(7, 7)}, {LaneID: "npcs", SourceRef: ref(10, 10)}}},
		},
		{
			name: "duplicate contributions retain unique lane attribution",
			evidence: []LaneEvidence{
				{LaneID: "spells", SourceRefs: []source.SourceRef{ref(30, 30), ref(30, 30)}},
				{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(30, 30)}},
			},
			wantUnits: [][]int{{30}},
			wantRefs:  [][]EvidenceRef{{{LaneID: "npcs", SourceRef: ref(30, 30)}, {LaneID: "spells", SourceRef: ref(30, 30)}}},
		},
		{
			name:      "empty contributions retain explicit empty contexts",
			evidence:  []LaneEvidence{{LaneID: "npcs"}},
			wantUnits: [][]int{},
			wantRefs:  [][]EvidenceRef{},
		},
		{
			name:      "largest window clips without overflow",
			window:    math.MaxInt,
			evidence:  []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(30, 30)}}},
			wantUnits: [][]int{{10, 3, 30, 7, 50}},
			wantRefs:  [][]EvidenceRef{{{LaneID: "npcs", SourceRef: ref(30, 30)}}},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			document := testDocument(t)
			got, err := Build(BuildRequest{Source: document, WindowUnits: test.window, SelectedLanes: []string{"spells", "npcs"}, LaneEvidence: test.evidence})
			if err != nil {
				t.Fatalf("Build() error = %v", err)
			}
			if want := []string{"npcs", "spells"}; !reflect.DeepEqual(got.SelectedLanes, want) {
				t.Fatalf("SelectedLanes = %#v, want %#v", got.SelectedLanes, want)
			}
			if got.WindowUnits != test.window || got.SourceID != document.ID || got.SourceDigest != document.Digest {
				t.Fatalf("Build() identity = %#v, want source and window identity", got)
			}
			if actual := contextUnitIDs(got.Contexts); !reflect.DeepEqual(actual, test.wantUnits) {
				t.Fatalf("context unit ids = %#v, want %#v", actual, test.wantUnits)
			}
			if actual := contextEvidenceRefs(got.Contexts); !reflect.DeepEqual(actual, test.wantRefs) {
				t.Fatalf("context evidence refs = %#v, want %#v", actual, test.wantRefs)
			}
		})
	}
}

func TestBuildIsStableAndOwnsSourceAndInputs(t *testing.T) {
	document := testDocument(t)
	refs := []source.SourceRef{ref(30, 30), ref(3, 3)}
	request := BuildRequest{
		Source:        document,
		WindowUnits:   1,
		SelectedLanes: []string{"spells", "npcs"},
		LaneEvidence:  []LaneEvidence{{LaneID: "spells", SourceRefs: refs}, {LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}},
	}
	first, err := Build(request)
	if err != nil {
		t.Fatal(err)
	}
	secondRequest := request
	secondRequest.LaneEvidence = []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}, {LaneID: "spells", SourceRefs: []source.SourceRef{ref(3, 3), ref(30, 30)}}}
	second, err := Build(secondRequest)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(first, second) {
		t.Fatalf("Build() order differs:\nfirst: %#v\nsecond: %#v", first, second)
	}
	first.SelectedLanes[0] = "changed"
	first.Contexts[0].Units[0].Metadata["nested"].(map[string]any)["value"] = "changed"
	if document.Units[0].Metadata["nested"].(map[string]any)["value"] != "original" {
		t.Fatal("Build() returned metadata aliases to source document")
	}
	document.Units[0].Metadata["nested"].(map[string]any)["value"] = "later"
	if second.Contexts[0].Units[0].Metadata["nested"].(map[string]any)["value"] != "original" {
		t.Fatal("Build() retained metadata aliases to source document")
	}
	refs[0].StartUnitID = 999
	if !containsEvidenceRef(second.Contexts[0].EvidenceRefs, ref(30, 30)) {
		t.Fatal("Build() retained source-reference input aliases")
	}
}

func TestBuildRejectsInvalidInputs(t *testing.T) {
	for _, test := range []struct {
		name   string
		mutate func(*BuildRequest)
		want   string
	}{
		{name: "negative window", mutate: func(request *BuildRequest) { request.WindowUnits = -1 }, want: "window_units"},
		{name: "blank selected lane", mutate: func(request *BuildRequest) { request.SelectedLanes = []string{" "} }, want: "selected_lanes"},
		{name: "duplicate selected lane", mutate: func(request *BuildRequest) { request.SelectedLanes = []string{"npcs", " npcs "} }, want: "duplicated"},
		{name: "unselected contribution", mutate: func(request *BuildRequest) {
			request.LaneEvidence = []LaneEvidence{{LaneID: "other", SourceRefs: []source.SourceRef{ref(3, 3)}}}
		}, want: "not selected"},
		{name: "source digest mismatch", mutate: func(request *BuildRequest) { request.Source.Digest = "sha256:" + strings.Repeat("0", 64) }, want: "does not match"},
		{name: "invalid reference", mutate: func(request *BuildRequest) {
			request.LaneEvidence = []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(99, 99)}}}
		}, want: "source reference[0]"},
	} {
		t.Run(test.name, func(t *testing.T) {
			request := BuildRequest{Source: testDocument(t), SelectedLanes: []string{"npcs"}, LaneEvidence: []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}}}
			test.mutate(&request)
			if _, err := Build(request); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("Build() error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestCodecRoundTripsCompactFixtureAndOwnsDecodedValues(t *testing.T) {
	fixture, err := os.ReadFile("testdata/source_evidence_context.v1.json")
	if err != nil {
		t.Fatal(err)
	}
	codec := New()
	value, err := codec.Decode(fixture)
	if err != nil {
		t.Fatalf("Decode(fixture) error = %v", err)
	}
	encoded, err := codec.Encode(value)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(encoded, bytes.TrimSpace(fixture)) {
		t.Fatalf("fixture does not use canonical encoding\nwant: %s\n got: %s", fixture, encoded)
	}
	value.Contexts[0].Units[0].Text = "changed"
	decoded, err := codec.Decode(fixture)
	if err != nil {
		t.Fatal(err)
	}
	if decoded.Contexts[0].Units[0].Text != "The party meets Rowan." {
		t.Fatal("Decode() reused mutable document storage")
	}
	built, err := Build(BuildRequest{Source: testDocument(t), WindowUnits: 1, SelectedLanes: []string{"npcs"}, LaneEvidence: []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}}})
	if err != nil {
		t.Fatal(err)
	}
	content, err := codec.Encode(built)
	if err != nil {
		t.Fatal(err)
	}
	first, err := codec.Decode(content)
	if err != nil {
		t.Fatal(err)
	}
	second, err := codec.Decode(content)
	if err != nil {
		t.Fatal(err)
	}
	first.Contexts[0].Units[0].Metadata["nested"].(map[string]any)["value"] = "changed"
	if second.Contexts[0].Units[0].Metadata["nested"].(map[string]any)["value"] != "original" {
		t.Fatal("Decode() returned metadata aliases")
	}
}

func TestCodecRejectsInvalidDurableBoundaries(t *testing.T) {
	value, err := Build(BuildRequest{Source: testDocument(t), SelectedLanes: []string{"npcs"}, LaneEvidence: []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}}})
	if err != nil {
		t.Fatal(err)
	}
	for _, test := range []struct {
		name   string
		mutate func(*Document)
	}{
		{name: "unsorted lanes", mutate: func(value *Document) { value.SelectedLanes = []string{"z", "a"} }},
		{name: "context range mismatch", mutate: func(value *Document) { value.Contexts[0].ContextRef.EndUnitID = 999 }},
		{name: "mismatched evidence source", mutate: func(value *Document) { value.Contexts[0].EvidenceRefs[0].SourceRef.SourceID = "other" }},
		{name: "invalid evidence range", mutate: func(value *Document) {
			value.Contexts[0].EvidenceRefs[0].SourceRef.StartUnitID = 10
		}},
		{name: "duplicate context unit", mutate: func(value *Document) { value.Contexts = append(value.Contexts, value.Contexts[0]) }},
	} {
		t.Run(test.name, func(t *testing.T) {
			candidate, err := clone(value)
			if err != nil {
				t.Fatal(err)
			}
			test.mutate(&candidate)
			if _, err := New().Encode(candidate); err == nil {
				t.Fatal("Encode() error = nil, want durable model rejection")
			}
		})
	}
	content, err := New().Encode(value)
	if err != nil {
		t.Fatal(err)
	}
	for _, test := range []struct {
		name   string
		mutate func(map[string]any)
	}{
		{name: "missing contexts", mutate: func(value map[string]any) { delete(value, "contexts") }},
		{name: "null contexts", mutate: func(value map[string]any) { value["contexts"] = nil }},
		{name: "unknown fixed field", mutate: func(value map[string]any) { value["unknown"] = true }},
		{name: "missing units", mutate: func(value map[string]any) { delete(contextObject(value, 0), "units") }},
		{name: "null evidence refs", mutate: func(value map[string]any) { contextObject(value, 0)["evidence_refs"] = nil }},
	} {
		t.Run(test.name, func(t *testing.T) {
			raw := decodeJSON(t, content)
			test.mutate(raw)
			mutated, err := json.Marshal(raw)
			if err != nil {
				t.Fatal(err)
			}
			if _, err := New().Decode(mutated); err == nil {
				t.Fatal("Decode() error = nil, want strict payload rejection")
			}
		})
	}
	if _, err := New().Decode(append(content, []byte(" {}")...)); err == nil {
		t.Fatal("Decode() error = nil, want trailing JSON rejection")
	}
}

func TestSerializeUsesFixedArtifactIdentity(t *testing.T) {
	artifact, err := Serialize(BuildRequest{Source: testDocument(t), SelectedLanes: []string{"npcs"}})
	if err != nil {
		t.Fatal(err)
	}
	if artifact.Kind != ArtifactKind || artifact.MediaType != MediaType || artifact.Schema.ID != SchemaID || artifact.Schema.Name != SchemaName || artifact.Schema.Version != SchemaVersion {
		t.Fatalf("Serialize() = %#v, want fixed artifact identity", artifact)
	}
	decoded, err := New().Decode(artifact.Content)
	if err != nil || len(decoded.Contexts) != 0 || decoded.Contexts == nil {
		t.Fatalf("Decode(Serialize()) = %#v, %v; want explicit empty contexts", decoded, err)
	}
}

func testDocument(t *testing.T) *source.SourceDocument {
	t.Helper()
	document := &source.SourceDocument{
		ID: "session", Kind: "transcript", Format: "application/json",
		Units: []source.SourceUnit{
			{ID: 10, Kind: "segment", Text: "first", Ref: ref(10, 10), Metadata: map[string]any{"nested": map[string]any{"value": "original"}}},
			{ID: 3, Kind: "segment", Text: "second", Ref: ref(3, 3)},
			{ID: 30, Kind: "segment", Text: "third", Ref: ref(30, 30)},
			{ID: 7, Kind: "segment", Text: "fourth", Ref: ref(7, 7)},
			{ID: 50, Kind: "segment", Text: "fifth", Ref: ref(50, 50)},
		},
	}
	digest, err := source.DigestDocument(document)
	if err != nil {
		t.Fatal(err)
	}
	document.Digest = digest
	return document
}

func ref(start, end int) source.SourceRef {
	return source.SourceRef{SourceID: "session", StartUnitID: start, EndUnitID: end}
}

func contextUnitIDs(contexts []Context) [][]int {
	values := make([][]int, len(contexts))
	for index, context := range contexts {
		values[index] = make([]int, len(context.Units))
		for unitIndex, unit := range context.Units {
			values[index][unitIndex] = unit.ID
		}
	}
	return values
}

func contextEvidenceRefs(contexts []Context) [][]EvidenceRef {
	values := make([][]EvidenceRef, len(contexts))
	for index, context := range contexts {
		values[index] = append([]EvidenceRef(nil), context.EvidenceRefs...)
	}
	return values
}

func containsEvidenceRef(values []EvidenceRef, want source.SourceRef) bool {
	for _, value := range values {
		if value.SourceRef == want {
			return true
		}
	}
	return false
}

func decodeJSON(t *testing.T, content []byte) map[string]any {
	t.Helper()
	decoder := json.NewDecoder(bytes.NewReader(content))
	decoder.UseNumber()
	var value map[string]any
	if err := decoder.Decode(&value); err != nil {
		t.Fatal(err)
	}
	return value
}

func contextObject(value map[string]any, index int) map[string]any {
	return value["contexts"].([]any)[index].(map[string]any)
}
