package evidencecontext

import (
	"bytes"
	"math"
	"os"
	"reflect"
	"strings"
	"testing"

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

func TestBuildSelectsExpandedSourceUnitUnion(t *testing.T) {
	for _, test := range []struct {
		name    string
		window  int
		refs    []source.SourceRef
		wantIDs []int
	}{
		{name: "zero window", refs: []source.SourceRef{ref(3, 3)}, wantIDs: []int{3}},
		{name: "multi unit citation includes complete range", refs: []source.SourceRef{ref(3, 7)}, wantIDs: []int{3, 30, 7}},
		{name: "non monotonic IDs use document positions", window: 1, refs: []source.SourceRef{ref(3, 3)}, wantIDs: []int{10, 3, 30}},
		{name: "boundary clamping", window: 1, refs: []source.SourceRef{ref(10, 10), ref(50, 50)}, wantIDs: []int{10, 3, 7, 50}},
		{name: "overlapping and adjacent windows merge", window: 1, refs: []source.SourceRef{ref(3, 3), ref(30, 30), ref(30, 30)}, wantIDs: []int{10, 3, 30, 7}},
		{name: "adjacent expanded ranges merge", window: 1, refs: []source.SourceRef{ref(10, 10), ref(7, 7)}, wantIDs: []int{10, 3, 30, 7, 50}},
		{name: "largest window clips without overflow", window: math.MaxInt, refs: []source.SourceRef{ref(30, 30)}, wantIDs: []int{10, 3, 30, 7, 50}},
		{name: "no references returns an initialized empty document", wantIDs: []int{}},
	} {
		t.Run(test.name, func(t *testing.T) {
			got, err := Build(BuildRequest{Source: testDocument(t), WindowUnits: test.window, SourceRefs: test.refs})
			if err != nil {
				t.Fatalf("Build() error = %v", err)
			}
			if got == nil {
				t.Fatal("Build() returned a nil document")
			}
			if actual := unitIDs(got); !reflect.DeepEqual(actual, test.wantIDs) {
				t.Fatalf("unit IDs = %#v, want %#v", actual, test.wantIDs)
			}
		})
	}
}

func TestBuildCopiesSelectedUnitsAndMetadata(t *testing.T) {
	document := testDocument(t)
	first, err := Build(BuildRequest{Source: document, WindowUnits: 1, SourceRefs: []source.SourceRef{ref(3, 3)}})
	if err != nil {
		t.Fatal(err)
	}
	second, err := Build(BuildRequest{Source: document, WindowUnits: 1, SourceRefs: []source.SourceRef{ref(3, 3)}})
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(first[0], document.Units[0]) {
		t.Fatalf("first unit = %#v, want unchanged source unit %#v", first[0], document.Units[0])
	}
	first[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 the source document")
	}
	document.Units[0].Metadata["nested"].(map[string]any)["value"] = "later"
	if second[0].Metadata["nested"].(map[string]any)["value"] != "original" {
		t.Fatal("Build() retained metadata aliases to the source document")
	}
}

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: "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.SourceRefs = []source.SourceRef{ref(99, 99)} }, want: "source reference[0]"},
	} {
		t.Run(test.name, func(t *testing.T) {
			request := BuildRequest{Source: testDocument(t), 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 TestCodecRoundTripsFixtureAndOwnsValues(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[0].Text = "changed"
	decoded, err := codec.Decode(encoded)
	if err != nil {
		t.Fatal(err)
	}
	if decoded[0].Text != "The party meets Rowan." {
		t.Fatal("Encode() retained mutable document storage")
	}

	built, err := Build(BuildRequest{Source: testDocument(t), WindowUnits: 1, 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[0].Metadata["nested"].(map[string]any)["value"] = "changed"
	if second[0].Metadata["nested"].(map[string]any)["value"] != "original" {
		t.Fatal("Decode() returned metadata aliases")
	}
}

func TestCodecRejectsInvalidDurablePayloads(t *testing.T) {
	for _, test := range []struct {
		name    string
		content string
	}{
		{name: "null", content: "null"},
		{name: "wrapper object", content: `{"units":[]}`},
		{name: "missing required unit field", content: `[{"id":1,"kind":"segment","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1}}]`},
		{name: "unknown unit field", content: `[{"id":1,"kind":"segment","text":"text","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1},"unknown":true}]`},
		{name: "unknown reference field", content: `[{"id":1,"kind":"segment","text":"text","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1,"unknown":true}}]`},
		{name: "invalid self reference", content: `[{"id":1,"kind":"segment","text":"text","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":2}}]`},
		{name: "mixed source documents", content: `[{"id":1,"kind":"segment","text":"one","ref":{"source_id":"session-one","start_unit_id":1,"end_unit_id":1}},{"id":2,"kind":"segment","text":"two","ref":{"source_id":"session-two","start_unit_id":2,"end_unit_id":2}}]`},
		{name: "duplicate units", content: `[{"id":1,"kind":"segment","text":"one","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1}},{"id":1,"kind":"segment","text":"two","ref":{"source_id":"session","start_unit_id":1,"end_unit_id":1}}]`},
		{name: "multiple JSON values", content: `[] []`},
	} {
		t.Run(test.name, func(t *testing.T) {
			if _, err := New().Decode([]byte(test.content)); err == nil {
				t.Fatal("Decode() error = nil, want strict payload rejection")
			}
		})
	}
	if _, err := New().Encode(nil); err == nil {
		t.Fatal("Encode(nil) error = nil, want array rejection")
	}
}

func TestSerializeUsesFixedArtifactIdentityAndEmptyArray(t *testing.T) {
	artifact, err := Serialize(BuildRequest{Source: testDocument(t)})
	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)
	}
	if string(artifact.Content) != "[]" {
		t.Fatalf("Serialize() content = %s, want []", artifact.Content)
	}
	decoded, err := New().Decode(artifact.Content)
	if err != nil || decoded == nil || len(decoded) != 0 {
		t.Fatalf("Decode(Serialize()) = %#v, %v; want explicit empty array", 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 unitIDs(units Document) []int {
	values := make([]int, len(units))
	for index, unit := range units {
		values[index] = unit.ID
	}
	return values
}
