package semanticreconcile

import (
	"errors"
	"reflect"
	"strings"
	"testing"
)

type syntheticRecord struct {
	Name  string
	Notes []string
}

func cloneSyntheticRecord(record syntheticRecord) syntheticRecord {
	record.Notes = cloneSlice(record.Notes)
	return record
}

func TestNewRecordOwnsValueAndNormalizesProvenance(t *testing.T) {
	value := syntheticRecord{Name: "alpha", Notes: []string{"owned"}}
	indexes := []int{3, 1, 3}
	record, err := NewRecord(value, indexes, 4, cloneSyntheticRecord)
	if err != nil {
		t.Fatalf("NewRecord() error = %v", err)
	}

	value.Notes[0] = "caller mutation"
	indexes[0] = 99
	gotValue := record.Value()
	gotIndexes := record.OriginalInputIndexes()
	if gotValue.Name != "alpha" || !reflect.DeepEqual(gotValue.Notes, []string{"owned"}) {
		t.Fatalf("Value() = %#v, want owned original", gotValue)
	}
	if !reflect.DeepEqual(gotIndexes, []int{1, 3}) {
		t.Fatalf("OriginalInputIndexes() = %v, want [1 3]", gotIndexes)
	}
	if record.EarliestInputPosition() != 4 {
		t.Fatalf("EarliestInputPosition() = %d, want 4", record.EarliestInputPosition())
	}

	gotValue.Notes[0] = "accessor mutation"
	gotIndexes[0] = 88
	if again := record.Value(); again.Notes[0] != "owned" {
		t.Fatalf("Value() retained accessor mutation: %#v", again)
	}
	if again := record.OriginalInputIndexes(); !reflect.DeepEqual(again, []int{1, 3}) {
		t.Fatalf("OriginalInputIndexes() retained accessor mutation: %v", again)
	}
}

func TestNewRecordPreservesNilAndEmptyIndexOwnership(t *testing.T) {
	nilIndexes, err := NewRecord(syntheticRecord{}, nil, 0, cloneSyntheticRecord)
	if err != nil {
		t.Fatalf("NewRecord(nil) error = %v", err)
	}
	emptyIndexes, err := NewRecord(syntheticRecord{}, []int{}, 0, cloneSyntheticRecord)
	if err != nil {
		t.Fatalf("NewRecord(empty) error = %v", err)
	}
	if nilIndexes.OriginalInputIndexes() != nil {
		t.Fatal("nil original indexes became non-nil")
	}
	if got := emptyIndexes.OriginalInputIndexes(); got == nil || len(got) != 0 {
		t.Fatalf("empty original indexes = %#v, want non-nil empty", got)
	}

	if _, err := NewRecord(syntheticRecord{}, nil, 0, CloneValueFunc[syntheticRecord](nil)); err == nil {
		t.Fatal("NewRecord() accepted nil clone function")
	}
	if _, err := NewRecord(syntheticRecord{}, nil, -1, cloneSyntheticRecord); err == nil {
		t.Fatal("NewRecord() accepted negative earliest position")
	}
	if _, err := NewRecord(syntheticRecord{}, []int{-1}, 0, cloneSyntheticRecord); err == nil {
		t.Fatal("NewRecord() accepted negative original input index")
	}
}

func TestApplyPlanConsolidatesGroupsAndOrdersByEarliestContribution(t *testing.T) {
	records := syntheticRecords(t,
		recordFixture{"alpha", []int{4}, 4},
		recordFixture{"bravo", []int{3, 1}, 1},
		recordFixture{"charlie", []int{2}, 2},
		recordFixture{"delta", []int{2, 0}, 0},
	)
	plan := Plan{groups: []PlanGroup{
		{memberPositions: []int{0, 2}, canonicalPosition: 2},
		{memberPositions: []int{1, 3}, canonicalPosition: 1},
	}}
	var canonicalNames []string
	result, err := ApplyPlan(plan, records, ApplicationPolicy[syntheticRecord]{
		CloneValue: cloneSyntheticRecord,
		ConsolidateGroup: func(members []syntheticRecord, canonical syntheticRecord) (syntheticRecord, error) {
			canonicalNames = append(canonicalNames, canonical.Name)
			canonical.Notes = []string{members[0].Name, members[1].Name}
			return canonical, nil
		},
	})
	if err != nil {
		t.Fatalf("ApplyPlan() error = %v", err)
	}

	got := result.Records()
	if names := recordNames(got); !reflect.DeepEqual(names, []string{"bravo", "charlie"}) {
		t.Fatalf("record names = %v, want [bravo charlie]", names)
	}
	if !reflect.DeepEqual(canonicalNames, []string{"charlie", "bravo"}) {
		t.Fatalf("canonical values = %v, want [charlie bravo]", canonicalNames)
	}
	if indexes := got[0].OriginalInputIndexes(); !reflect.DeepEqual(indexes, []int{0, 1, 2, 3}) {
		t.Fatalf("first provenance indexes = %v, want [0 1 2 3]", indexes)
	}
	if indexes := got[1].OriginalInputIndexes(); !reflect.DeepEqual(indexes, []int{2, 4}) {
		t.Fatalf("second provenance indexes = %v, want [2 4]", indexes)
	}
	if got[0].EarliestInputPosition() != 0 || got[1].EarliestInputPosition() != 2 {
		t.Fatalf("earliest positions = [%d %d], want [0 2]", got[0].EarliestInputPosition(), got[1].EarliestInputPosition())
	}

	events := result.AppliedGroups()
	if len(events) != 2 || len(result.RejectedGroups()) != 0 {
		t.Fatalf("event counts = applied %d rejected %d, want 2 and 0", len(events), len(result.RejectedGroups()))
	}
	first := events[0].Provenance()
	if !reflect.DeepEqual(first.MemberPositions(), []int{0, 2}) || first.CanonicalPosition() != 2 || !reflect.DeepEqual(first.OriginalInputIndexes(), []int{2, 4}) || first.EarliestInputPosition() != 2 {
		t.Fatalf("first applied provenance = members %v canonical %d indexes %v earliest %d", first.MemberPositions(), first.CanonicalPosition(), first.OriginalInputIndexes(), first.EarliestInputPosition())
	}
}

func TestApplyPlanWithoutGroupsReturnsOwnedRecordsInProvenanceOrder(t *testing.T) {
	records := syntheticRecords(t,
		recordFixture{"alpha", []int{2}, 2},
		recordFixture{"bravo", []int{0}, 0},
		recordFixture{"charlie", []int{1}, 1},
	)
	result, err := ApplyPlan(Plan{}, records, syntheticPolicy())
	if err != nil {
		t.Fatalf("ApplyPlan() error = %v", err)
	}
	if names := recordNames(result.Records()); !reflect.DeepEqual(names, []string{"bravo", "charlie", "alpha"}) {
		t.Fatalf("record names = %v, want [bravo charlie alpha]", names)
	}
	if result.AppliedGroups() != nil || result.RejectedGroups() != nil {
		t.Fatalf("events = applied %#v rejected %#v, want nil", result.AppliedGroups(), result.RejectedGroups())
	}

	value := result.Records()[0].Value()
	value.Notes[0] = "changed"
	if records[1].Value().Notes[0] != "bravo" || result.Records()[0].Value().Notes[0] != "bravo" {
		t.Fatal("no-group result shares mutable value state")
	}
}

func TestApplyPlanPreservesUngroupedRecordsAndOwnsResults(t *testing.T) {
	records := syntheticRecords(t,
		recordFixture{"alpha", []int{4}, 4},
		recordFixture{"bravo", []int{1}, 1},
		recordFixture{"charlie", []int{2}, 2},
	)
	result, err := ApplyPlan(Plan{groups: []PlanGroup{{memberPositions: []int{0, 2}, canonicalPosition: 0}}}, records, syntheticPolicy())
	if err != nil {
		t.Fatalf("ApplyPlan() error = %v", err)
	}
	if names := recordNames(result.Records()); !reflect.DeepEqual(names, []string{"bravo", "alpha"}) {
		t.Fatalf("record names = %v, want ungrouped bravo then consolidated alpha", names)
	}

	firstRead := result.Records()
	firstValue := firstRead[0].Value()
	firstValue.Notes[0] = "changed"
	firstRead[0].originalInputIndexes[0] = 99
	if again := result.Records(); again[0].Value().Notes[0] != "bravo" || !reflect.DeepEqual(again[0].OriginalInputIndexes(), []int{1}) {
		t.Fatalf("result retained accessor mutations: %#v", again[0])
	}
	if original := records[1].Value(); original.Notes[0] != "bravo" {
		t.Fatalf("input record was mutated: %#v", original)
	}
}

func TestApplyPlanGuardRejectionPreservesEveryMemberOnce(t *testing.T) {
	records := syntheticRecords(t,
		recordFixture{"alpha", []int{3}, 3},
		recordFixture{"bravo", []int{1}, 1},
		recordFixture{"charlie", []int{2}, 2},
	)
	consolidations := 0
	result, err := ApplyPlan(Plan{groups: []PlanGroup{{memberPositions: []int{0, 2}, canonicalPosition: 2}}}, records, ApplicationPolicy[syntheticRecord]{
		CloneValue: cloneSyntheticRecord,
		RejectGroup: func(members []syntheticRecord, canonical syntheticRecord) RejectionCategory {
			members[0].Notes[0] = "guard mutation"
			canonical.Notes[0] = "canonical mutation"
			return "typed_constraint"
		},
		ConsolidateGroup: func([]syntheticRecord, syntheticRecord) (syntheticRecord, error) {
			consolidations++
			return syntheticRecord{}, nil
		},
	})
	if err != nil {
		t.Fatalf("ApplyPlan() error = %v", err)
	}
	if consolidations != 0 {
		t.Fatalf("consolidation calls = %d, want 0", consolidations)
	}
	if names := recordNames(result.Records()); !reflect.DeepEqual(names, []string{"bravo", "charlie", "alpha"}) {
		t.Fatalf("preserved record names = %v, want [bravo charlie alpha]", names)
	}
	for index, record := range records {
		if got := record.Value().Notes[0]; got != record.Value().Name {
			t.Fatalf("input record %d note = %q after guard, want original", index, got)
		}
	}
	rejected := result.RejectedGroups()
	if len(rejected) != 1 || rejected[0].Category() != "typed_constraint" {
		t.Fatalf("rejected events = %#v, want typed_constraint", rejected)
	}
	provenance := rejected[0].Provenance()
	if !reflect.DeepEqual(provenance.MemberPositions(), []int{0, 2}) || !reflect.DeepEqual(provenance.OriginalInputIndexes(), []int{2, 3}) || provenance.EarliestInputPosition() != 2 {
		t.Fatalf("rejected provenance = members %v indexes %v earliest %d", provenance.MemberPositions(), provenance.OriginalInputIndexes(), provenance.EarliestInputPosition())
	}
}

func TestApplyPlanSuppliesFreshPolicyValuesAndDoesNotMutateInputs(t *testing.T) {
	records := syntheticRecords(t,
		recordFixture{"alpha", []int{0}, 0},
		recordFixture{"bravo", []int{1}, 1},
	)
	result, err := ApplyPlan(Plan{groups: []PlanGroup{{memberPositions: []int{0, 1}, canonicalPosition: 1}}}, records, ApplicationPolicy[syntheticRecord]{
		CloneValue: cloneSyntheticRecord,
		RejectGroup: func(members []syntheticRecord, canonical syntheticRecord) RejectionCategory {
			members[0].Name = "guard mutation"
			canonical.Name = "guard canonical mutation"
			return ""
		},
		ConsolidateGroup: func(members []syntheticRecord, canonical syntheticRecord) (syntheticRecord, error) {
			if members[0].Name != "alpha" || canonical.Name != "bravo" {
				t.Fatalf("consolidation observed guard mutations: members %#v canonical %#v", members, canonical)
			}
			members[0].Notes[0] = "consolidator mutation"
			canonical.Notes = []string{"result"}
			return canonical, nil
		},
	})
	if err != nil {
		t.Fatalf("ApplyPlan() error = %v", err)
	}
	if got := result.Records()[0].Value(); got.Name != "bravo" || !reflect.DeepEqual(got.Notes, []string{"result"}) {
		t.Fatalf("consolidated value = %#v", got)
	}
	if records[0].Value().Notes[0] != "alpha" || records[1].Value().Notes[0] != "bravo" {
		t.Fatalf("input records changed: %#v %#v", records[0].Value(), records[1].Value())
	}
}

func TestApplyPlanRejectsMalformedPlans(t *testing.T) {
	records := syntheticRecords(t,
		recordFixture{"alpha", nil, 0},
		recordFixture{"bravo", nil, 1},
		recordFixture{"charlie", nil, 2},
	)
	tests := []struct {
		name string
		plan Plan
		want string
	}{
		{name: "member out of range", plan: Plan{groups: []PlanGroup{{memberPositions: []int{0, 3}, canonicalPosition: 0}}}, want: "outside record range"},
		{name: "canonical out of range", plan: Plan{groups: []PlanGroup{{memberPositions: []int{0, 1}, canonicalPosition: 3}}}, want: "canonical position"},
		{name: "canonical not a member", plan: Plan{groups: []PlanGroup{{memberPositions: []int{0, 1}, canonicalPosition: 2}}}, want: "is not a member"},
		{name: "duplicate member", plan: Plan{groups: []PlanGroup{{memberPositions: []int{0, 0}, canonicalPosition: 0}}}, want: "ascending and unique"},
		{name: "overlapping groups", plan: Plan{groups: []PlanGroup{{memberPositions: []int{0, 1}, canonicalPosition: 0}, {memberPositions: []int{1, 2}, canonicalPosition: 1}}}, want: "multiple groups"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := ApplyPlan(test.plan, records, syntheticPolicy())
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("ApplyPlan() error = %v, want containing %q", err, test.want)
			}
		})
	}
}

func TestApplyPlanReturnsConsolidationFailures(t *testing.T) {
	records := syntheticRecords(t,
		recordFixture{"alpha", nil, 0},
		recordFixture{"bravo", nil, 1},
	)
	want := errors.New("cannot consolidate")
	result, err := ApplyPlan(Plan{groups: []PlanGroup{{memberPositions: []int{0, 1}, canonicalPosition: 0}}}, records, ApplicationPolicy[syntheticRecord]{
		CloneValue: cloneSyntheticRecord,
		ConsolidateGroup: func([]syntheticRecord, syntheticRecord) (syntheticRecord, error) {
			return syntheticRecord{}, want
		},
	})
	if !errors.Is(err, want) || !strings.Contains(err.Error(), "position 0") {
		t.Fatalf("ApplyPlan() error = %v, want contextual wrapped failure", err)
	}
	if result.Records() != nil || result.AppliedGroups() != nil || result.RejectedGroups() != nil {
		t.Fatalf("ApplyPlan() partial result = %#v, want zero result", result)
	}
}

func TestApplyPlanPreservesNilAndEmptyRecordCollections(t *testing.T) {
	policy := syntheticPolicy()
	nilResult, err := ApplyPlan(Plan{}, []Record[syntheticRecord](nil), policy)
	if err != nil {
		t.Fatalf("ApplyPlan(nil) error = %v", err)
	}
	emptyResult, err := ApplyPlan(Plan{}, []Record[syntheticRecord]{}, policy)
	if err != nil {
		t.Fatalf("ApplyPlan(empty) error = %v", err)
	}
	if nilResult.Records() != nil {
		t.Fatal("nil records became non-nil")
	}
	if got := emptyResult.Records(); got == nil || len(got) != 0 {
		t.Fatalf("empty records = %#v, want non-nil empty", got)
	}
}

func TestApplyPlanValidatesPolicyAndRecordState(t *testing.T) {
	valid := syntheticPolicy()
	if _, err := ApplyPlan(Plan{}, []Record[syntheticRecord]{}, ApplicationPolicy[syntheticRecord]{ConsolidateGroup: valid.ConsolidateGroup}); err == nil {
		t.Fatal("ApplyPlan() accepted nil clone function")
	}
	if _, err := ApplyPlan(Plan{}, []Record[syntheticRecord]{}, ApplicationPolicy[syntheticRecord]{CloneValue: cloneSyntheticRecord}); err == nil {
		t.Fatal("ApplyPlan() accepted nil consolidate function")
	}
	if _, err := ApplyPlan(Plan{}, []Record[syntheticRecord]{{}}, valid); err == nil || !strings.Contains(err.Error(), "record 0") {
		t.Fatalf("ApplyPlan() invalid record error = %v", err)
	}

	records := syntheticRecords(t,
		recordFixture{"alpha", nil, 0},
		recordFixture{"bravo", nil, 1},
	)
	valid.RejectGroup = func([]syntheticRecord, syntheticRecord) RejectionCategory { return " " }
	if _, err := ApplyPlan(Plan{groups: []PlanGroup{{memberPositions: []int{0, 1}, canonicalPosition: 0}}}, records, valid); err == nil || !strings.Contains(err.Error(), "category") {
		t.Fatalf("ApplyPlan() blank category error = %v", err)
	}
}

type recordFixture struct {
	name     string
	indexes  []int
	earliest int
}

func syntheticRecords(t *testing.T, fixtures ...recordFixture) []Record[syntheticRecord] {
	t.Helper()
	records := make([]Record[syntheticRecord], len(fixtures))
	for index, fixture := range fixtures {
		record, err := NewRecord(syntheticRecord{Name: fixture.name, Notes: []string{fixture.name}}, fixture.indexes, fixture.earliest, cloneSyntheticRecord)
		if err != nil {
			t.Fatalf("NewRecord(%d) error = %v", index, err)
		}
		records[index] = record
	}
	return records
}

func syntheticPolicy() ApplicationPolicy[syntheticRecord] {
	return ApplicationPolicy[syntheticRecord]{
		CloneValue: cloneSyntheticRecord,
		ConsolidateGroup: func(_ []syntheticRecord, canonical syntheticRecord) (syntheticRecord, error) {
			return canonical, nil
		},
	}
}

func recordNames(records []Record[syntheticRecord]) []string {
	names := make([]string, len(records))
	for index, record := range records {
		names[index] = record.Value().Name
	}
	return names
}
