package source

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

func TestValidateDocumentValid(t *testing.T) {
	doc := validDocument()

	if err := ValidateDocument(doc); err != nil {
		t.Fatalf("ValidateDocument() error = %v, want nil", err)
	}
}

func TestValidateDocumentNil(t *testing.T) {
	err := ValidateDocument(nil)

	requireErrorFragments(t, err, "source document", "nil")
}

func TestValidateDocumentMissingFields(t *testing.T) {
	tests := []struct {
		name      string
		mutate    func(*SourceDocument)
		fragments []string
	}{
		{
			name:      "id",
			mutate:    func(doc *SourceDocument) { doc.ID = " \t" },
			fragments: []string{"source document id", "must not be empty"},
		},
		{
			name:      "id surrounding whitespace",
			mutate:    func(doc *SourceDocument) { doc.ID = " source-1 " },
			fragments: []string{"source document id", "leading or trailing whitespace"},
		},
		{
			name:      "kind",
			mutate:    func(doc *SourceDocument) { doc.Kind = "" },
			fragments: []string{"source document kind", "must not be empty"},
		},
		{
			name:      "format",
			mutate:    func(doc *SourceDocument) { doc.Format = "\n" },
			fragments: []string{"source document format", "must not be empty"},
		},
		{
			name:      "digest",
			mutate:    func(doc *SourceDocument) { doc.Digest = "" },
			fragments: []string{"source document digest", "must not be empty"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			doc := validDocument()
			tt.mutate(doc)

			err := ValidateDocument(doc)

			requireErrorFragments(t, err, tt.fragments...)
		})
	}
}

func TestValidateDocumentEmptyUnits(t *testing.T) {
	doc := validDocument()
	doc.Units = nil

	err := ValidateDocument(doc)

	requireErrorFragments(t, err, "source document units", "must not be empty")
}

func TestValidateDocumentMissingUnitFields(t *testing.T) {
	tests := []struct {
		name      string
		mutate    func(*SourceDocument)
		fragments []string
	}{
		{
			name:      "id",
			mutate:    func(doc *SourceDocument) { doc.Units[1].ID = 0 },
			fragments: []string{"source unit[1].id", "must be positive"},
		},
		{
			name:      "kind",
			mutate:    func(doc *SourceDocument) { doc.Units[1].Kind = " " },
			fragments: []string{"source unit[1].kind", "must not be empty"},
		},
		{
			name:      "text",
			mutate:    func(doc *SourceDocument) { doc.Units[1].Text = "\n\t" },
			fragments: []string{"source unit[1].text", "must not be empty"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			doc := validDocument()
			tt.mutate(doc)

			err := ValidateDocument(doc)

			requireErrorFragments(t, err, tt.fragments...)
		})
	}
}

func TestValidateDocumentDuplicateUnitIDs(t *testing.T) {
	doc := validDocument()
	doc.Units[1].ID = 1

	err := ValidateDocument(doc)

	requireErrorFragments(t, err, "source unit id 1", "duplicated")
}

func TestValidateDocumentUnitReferences(t *testing.T) {
	tests := []struct {
		name      string
		mutate    func(*SourceDocument)
		fragments []string
	}{
		{
			name:      "nested reference context",
			mutate:    func(doc *SourceDocument) { doc.Units[0].Ref.SourceID = "source-2" },
			fragments: []string{"source unit[0].ref", "source_id", "does not match"},
		},
		{
			name: "document unit self-reference",
			mutate: func(doc *SourceDocument) {
				doc.Units[0].Ref.StartUnitID = 2
				doc.Units[0].Ref.EndUnitID = 2
			},
			fragments: []string{"source unit[0].ref", "must identify source unit id 1"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			doc := validDocument()
			tt.mutate(doc)

			err := ValidateDocument(doc)
			requireErrorFragments(t, err, tt.fragments...)
		})
	}
}

func TestDigestDocumentIsDeterministicAndIncludesUnitReference(t *testing.T) {
	doc := validDocument()
	doc.Metadata = map[string]any{"second": "value", "first": true}
	first, err := DigestDocument(doc)
	if err != nil {
		t.Fatalf("DigestDocument() error = %v, want nil", err)
	}

	reordered := validDocument()
	reordered.Metadata = map[string]any{"first": true, "second": "value"}
	second, err := DigestDocument(reordered)
	if err != nil {
		t.Fatalf("DigestDocument(reordered) error = %v, want nil", err)
	}
	if first != second {
		t.Fatalf("digests = %q and %q, want deterministic map ordering", first, second)
	}

	changed := validDocument()
	changed.Metadata = map[string]any{"first": true, "second": "value"}
	changed.Units[0].Ref.SourceID = "different-source"
	changedDigest, err := DigestDocument(changed)
	if err != nil {
		t.Fatalf("DigestDocument(changed) error = %v, want nil", err)
	}
	if first == changedDigest {
		t.Fatalf("digest = %q after reference change, want different digest", changedDigest)
	}
}

func TestDigestChunkIsDeterministicAndIncludesReference(t *testing.T) {
	doc := validDocument()
	chunk := Chunk{
		ID:        "chunk-1",
		SourceID:  doc.ID,
		Index:     0,
		Ref:       SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2},
		Content:   []byte("chunk content"),
		MediaType: "text/plain",
		Units:     doc.Units,
		Metadata:  map[string]any{"second": "value", "first": true},
	}
	first, err := DigestChunk(chunk)
	if err != nil {
		t.Fatalf("DigestChunk() error = %v, want nil", err)
	}

	chunk.Metadata = map[string]any{"first": true, "second": "value"}
	second, err := DigestChunk(chunk)
	if err != nil {
		t.Fatalf("DigestChunk(reordered metadata) error = %v, want nil", err)
	}
	if first != second {
		t.Fatalf("digests = %q and %q, want deterministic map ordering", first, second)
	}

	chunk.Ref.EndUnitID = 1
	changed, err := DigestChunk(chunk)
	if err != nil {
		t.Fatalf("DigestChunk(changed ref) error = %v, want nil", err)
	}
	if first == changed {
		t.Fatalf("digest = %q after reference change, want different digest", changed)
	}
}

func TestDigestChunkIncludesAnnotationScopes(t *testing.T) {
	doc := validDocument()
	chunk := Chunk{
		ID: "chunk-1", SourceID: doc.ID, Ref: SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2},
		Content: []byte("content"), MediaType: "text/plain", Units: doc.Units,
		Annotations:     ChunkAnnotations{"scope": json.RawMessage(`{"value":1}`)},
		PlanAnnotations: ChunkAnnotations{"scope": json.RawMessage(`{"value":2}`)},
	}
	base, err := DigestChunk(chunk)
	if err != nil {
		t.Fatalf("DigestChunk() error = %v", err)
	}
	chunk.Annotations["scope"] = json.RawMessage(`{"value":3}`)
	rangeChanged, _ := DigestChunk(chunk)
	chunk.Annotations["scope"] = json.RawMessage(`{"value":1}`)
	chunk.PlanAnnotations["scope"] = json.RawMessage(`{"value":3}`)
	planChanged, _ := DigestChunk(chunk)
	if base == rangeChanged || base == planChanged || rangeChanged == planChanged {
		t.Fatalf("annotation scope digests did not change distinctly: %q %q %q", base, rangeChanged, planChanged)
	}
}

func TestValidateRefValid(t *testing.T) {
	doc := validDocument()
	ref := SourceRef{
		SourceID:    "source-1",
		StartUnitID: 1,
		EndUnitID:   2,
	}

	if err := ValidateRef(doc, ref); err != nil {
		t.Fatalf("ValidateRef() error = %v, want nil", err)
	}
	if err := NewDocumentIndex(doc).ValidateRef(ref); err != nil {
		t.Fatalf("DocumentIndex.ValidateRef() error = %v, want nil", err)
	}
}

func TestValidateRefRejectsMalformedReferences(t *testing.T) {
	tests := []struct {
		name      string
		ref       SourceRef
		fragments []string
	}{
		{
			name:      "missing source id",
			ref:       SourceRef{StartUnitID: 1, EndUnitID: 2},
			fragments: []string{"source_id", "must not be empty"},
		},
		{
			name:      "source id surrounding whitespace",
			ref:       SourceRef{SourceID: " source-1 ", StartUnitID: 1, EndUnitID: 2},
			fragments: []string{"source_id", "leading or trailing whitespace"},
		},
		{
			name:      "missing start id",
			ref:       SourceRef{SourceID: "source-1", EndUnitID: 2},
			fragments: []string{"start_unit_id", "must be positive"},
		},
		{
			name:      "missing end id",
			ref:       SourceRef{SourceID: "source-1", StartUnitID: 1},
			fragments: []string{"end_unit_id", "must be positive"},
		},
		{
			name:      "unknown start id",
			ref:       SourceRef{SourceID: "source-1", StartUnitID: 9, EndUnitID: 2},
			fragments: []string{"start_unit_id", "was not found"},
		},
		{
			name:      "unknown end id",
			ref:       SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 9},
			fragments: []string{"end_unit_id", "was not found"},
		},
		{
			name:      "source id mismatch",
			ref:       SourceRef{SourceID: "source-2", StartUnitID: 1, EndUnitID: 2},
			fragments: []string{"source_id", "does not match"},
		},
		{
			name:      "reversed unit order",
			ref:       SourceRef{SourceID: "source-1", StartUnitID: 2, EndUnitID: 1},
			fragments: []string{"start_unit_id", "appears after"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			doc := validDocument()
			validators := []struct {
				name     string
				validate func(SourceRef) error
			}{
				{name: "document", validate: func(ref SourceRef) error { return ValidateRef(doc, ref) }},
				{name: "index", validate: NewDocumentIndex(doc).ValidateRef},
			}
			for _, validator := range validators {
				t.Run(validator.name, func(t *testing.T) {
					requireErrorFragments(t, validator.validate(tt.ref), tt.fragments...)
				})
			}
		})
	}
}

func TestDocumentIndexSnapshotsIdentityAndUnitPositions(t *testing.T) {
	doc := &SourceDocument{
		ID: "source-1",
		Units: []SourceUnit{
			{ID: 30},
			{ID: 10},
			{ID: 30},
		},
	}
	index := NewDocumentIndex(doc)
	doc.ID = "changed"
	doc.Units[0].ID = 99

	if documentID, ok := index.DocumentID(); !ok || documentID != "source-1" {
		t.Fatalf("DocumentID() = %q, %t, want source-1, true", documentID, ok)
	}
	if position, ok := index.Position(30); !ok || position != 0 {
		t.Fatalf("Position(30) = %d, %t, want 0, true", position, ok)
	}
	if position, ok := index.Position(10); !ok || position != 1 {
		t.Fatalf("Position(10) = %d, %t, want 1, true", position, ok)
	}
	ref := SourceRef{SourceID: "source-1", StartUnitID: 30, EndUnitID: 10}
	if err := index.ValidateRef(ref); err != nil {
		t.Fatalf("ValidateRef() error = %v, want nil", err)
	}
}

func TestZeroDocumentIndexIsSafe(t *testing.T) {
	var index DocumentIndex
	if documentID, ok := index.DocumentID(); ok || documentID != "" {
		t.Fatalf("DocumentID() = %q, %t, want empty, false", documentID, ok)
	}
	if position, ok := index.Position(1); ok || position != 0 {
		t.Fatalf("Position(1) = %d, %t, want 0, false", position, ok)
	}
	requireErrorFragments(t, index.ValidateRef(SourceRef{}), "source document must not be nil")
}

func TestUnitIndex(t *testing.T) {
	doc := validDocument()

	index, ok := UnitIndex(doc, 2)
	if !ok {
		t.Fatal("UnitIndex() ok = false, want true")
	}
	if index != 1 {
		t.Fatalf("UnitIndex() index = %d, want 1", index)
	}

	index, ok = UnitIndex(doc, 9)
	if ok {
		t.Fatal("UnitIndex() ok = true, want false")
	}
	if index != 0 {
		t.Fatalf("UnitIndex() index = %d, want 0", index)
	}
}

func validDocument() *SourceDocument {
	return &SourceDocument{
		ID:     "source-1",
		Kind:   "document",
		Format: "text/plain",
		Digest: "sha256:abc123",
		Units: []SourceUnit{
			{
				ID:   1,
				Kind: "paragraph",
				Text: "First unit.",
				Ref:  SourceRef{SourceID: "source-1", StartUnitID: 1, EndUnitID: 1},
			},
			{
				ID:   2,
				Kind: "paragraph",
				Text: "Second unit.",
				Ref:  SourceRef{SourceID: "source-1", StartUnitID: 2, EndUnitID: 2},
			},
		},
	}
}

func requireErrorFragments(t *testing.T, err error, fragments ...string) {
	t.Helper()
	if err == nil {
		t.Fatalf("error = nil, want fragments %q", fragments)
	}
	for _, fragment := range fragments {
		if !strings.Contains(err.Error(), fragment) {
			t.Fatalf("error = %q, want fragment %q", err.Error(), fragment)
		}
	}
}
