package analyticsauthoringv1

import (
	"context"
	"errors"
	"reflect"
	"regexp"
	"strings"
	"testing"
	"time"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func TestRequestRequiresTypedDefinitionAndCorrectIdentity(t *testing.T) {
	valid := validDashboardCreateRequest()
	zeroRevision := uint64(0)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := ValidateRequest(ctx, valid); err != nil {
		t.Fatal(err)
	}
	for _, candidate := range []Request{
		{Operation: CreateDashboard, Dashboard: valid.Dashboard},
		{CommandID: "bad command", Operation: CreateDashboard, Dashboard: valid.Dashboard},
		{CommandID: valid.CommandID, Operation: CreateDashboard, ResourceID: "unexpected", Dashboard: valid.Dashboard},
		{CommandID: valid.CommandID, Operation: UpdateFolder, Folder: &FolderDefinition{Name: stringPointer("Changed")}},
		{CommandID: valid.CommandID, Operation: DeleteFolder, ResourceID: "folder-1", Folder: &FolderDefinition{}},
		{CommandID: valid.CommandID, Operation: UpdateFolder, ResourceID: " folder-1", Folder: &FolderDefinition{Name: stringPointer("Changed")}},
		{CommandID: valid.CommandID, Operation: UpdateFolder, ResourceID: "folder-1", Folder: &FolderDefinition{ParentFolderID: stringPointer("folder-1")}},
		{CommandID: valid.CommandID, Operation: CreateDashboard, MinCatalogRevision: &zeroRevision, Dashboard: valid.Dashboard},
		{CommandID: valid.CommandID, Operation: CreateDashboard, Dashboard: valid.Dashboard, Folder: &FolderDefinition{}},
		{CommandID: valid.CommandID, Operation: UpdateDashboard, ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{}},
	} {
		var failure *common.Failure
		if err := ValidateRequest(ctx, candidate); !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest {
			t.Fatalf("ValidateRequest(%#v) = %#v", candidate, err)
		}
	}
}

func TestReportDefinitionIsTypedAndDigestBindsDateFilter(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	columns := []string{"Id", "Name"}
	filters := []ReportFilter{{Column: "Stage", Operator: "equals", Value: "Open"}}
	grouping := []string{"OwnerId"}
	request := Request{CommandID: "report-create-1", Operation: CreateReport, Report: &ReportDefinition{
		Name: stringPointer("Pipeline"), StableName: stringPointer("Pipeline"), FolderID: stringPointer("folder-1"),
		SourceObject: stringPointer("Opportunity"), SourceRef: stringPointer("OpportunityList"), DisplayMode: stringPointer("SUMMARY"),
		Columns: &columns, Filters: &filters, Grouping: &grouping,
		DateFilter: &ReportDateFilter{Column: "CloseDate", DurationValue: "CUSTOM", StartDate: "2026-01-01", EndDate: "2026-01-31"},
	}}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatal(err)
	}
	first, err := CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	changed := *request.Report
	dateFilter := *changed.DateFilter
	dateFilter.EndDate = "2026-02-01"
	changed.DateFilter = &dateFilter
	request.Report = &changed
	if second, err := CommandDigest(request); err != nil || second == first {
		t.Fatalf("changed report date filter digest = %q, %v", second, err)
	}
}

func TestReportDateFilterUpdateHasDistinctSetClearAndAbsentEffects(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	set := Request{CommandID: "report-date-set-1", Operation: UpdateReport, ResourceID: "report-1", Report: &ReportDefinition{
		Name:       stringPointer("Changed"),
		DateFilter: &ReportDateFilter{Column: "CloseDate", DurationValue: "CUSTOM", StartDate: "2026-01-01", EndDate: "2026-01-31"},
	}}
	clear := Request{CommandID: "report-date-clear-1", Operation: UpdateReport, ResourceID: "report-1", Report: &ReportDefinition{Name: stringPointer("Changed"), ClearDateFilter: true}}
	absent := validReportUpdateRequest()
	for _, request := range []Request{set, clear, absent} {
		if err := ValidateRequest(ctx, request); err != nil {
			t.Fatalf("ValidateRequest(%#v) = %v", request.Report, err)
		}
	}
	digests := map[string]struct{}{}
	for _, request := range []Request{set, clear, absent} {
		digest, err := CommandDigest(request)
		if err != nil {
			t.Fatal(err)
		}
		digests[digest] = struct{}{}
	}
	if len(digests) != 3 {
		t.Fatalf("set, clear, and absent produced %d distinct digests, want 3", len(digests))
	}
	for _, definition := range []*ReportDefinition{
		{DateFilter: set.Report.DateFilter, ClearDateFilter: true},
		{Name: stringPointer("Pipeline"), StableName: stringPointer("Pipeline"), FolderID: stringPointer("folder-1"), SourceObject: stringPointer("Opportunity"), ClearDateFilter: true},
	} {
		operation := UpdateReport
		resourceID := "report-1"
		if definition.Name != nil {
			operation, resourceID = CreateReport, ""
		}
		request := Request{CommandID: "invalid-date-filter-action", Operation: operation, ResourceID: resourceID, Report: definition}
		var failure *common.Failure
		if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest {
			t.Fatalf("invalid date-filter action = %#v", err)
		}
	}
}

func TestReportFolderIsRequiredOnCreateAndCannotBeCleared(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	create := Request{CommandID: "report-create-1", Operation: CreateReport, Report: &ReportDefinition{
		Name: stringPointer("Pipeline"), StableName: stringPointer("Pipeline"), SourceObject: stringPointer("Opportunity"),
	}}
	empty := ""
	update := Request{CommandID: "report-update-1", Operation: UpdateReport, ResourceID: "report-1", Report: &ReportDefinition{FolderID: &empty}}
	for _, request := range []Request{create, update} {
		var failure *common.Failure
		if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest {
			t.Fatalf("folderless report request = %#v", err)
		}
	}
	create.Report.FolderID = stringPointer("folder-1")
	if err := ValidateRequest(ctx, create); err != nil {
		t.Fatalf("report create with authorization target: %v", err)
	}
}

func TestDashboardComponentBoundaryIsSharedByCreateAndUpdate(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	components := make([]DashboardComponent, MaxDashboardComponents)
	for index := range components {
		components[index] = DashboardComponent{Kind: "CHART", Configuration: `{}`}
	}
	create := validDashboardCreateRequest()
	create.Dashboard.Components = &components
	if err := ValidateRequest(ctx, create); err != nil {
		t.Fatalf("maximum create graph: %v", err)
	}
	update := Request{CommandID: "dashboard-update-1", Operation: UpdateDashboard, ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{Components: &components}}
	if err := ValidateRequest(ctx, update); err != nil {
		t.Fatalf("maximum update graph: %v", err)
	}
	components = append(components, DashboardComponent{Kind: "CHART"})
	for _, request := range []Request{create, update} {
		request.Dashboard.Components = &components
		var failure *common.Failure
		if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted || failure.Maximum != MaxDashboardComponents {
			t.Fatalf("oversized graph = %#v", err)
		}
	}
}

type reportCollectionLimitCase struct {
	path       string
	definition func(int) *ReportDefinition
}

func TestReportDefinitionEntryBoundaryCoversEveryCollection(t *testing.T) {
	tests := []reportCollectionLimitCase{
		{
			path: "ReportDefinition.Columns",
			definition: func(count int) *ReportDefinition {
				values := make([]string, count)
				for index := range values {
					values[index] = "Name"
				}
				return &ReportDefinition{Columns: &values}
			},
		},
		{
			path: "ReportDefinition.Filters",
			definition: func(count int) *ReportDefinition {
				values := make([]ReportFilter, count)
				for index := range values {
					values[index] = ReportFilter{Column: "Name", Operator: "equals", Value: "Open"}
				}
				return &ReportDefinition{Filters: &values}
			},
		},
		{
			path: "ReportDefinition.Grouping",
			definition: func(count int) *ReportDefinition {
				values := make([]string, count)
				for index := range values {
					values[index] = "OwnerId"
				}
				return &ReportDefinition{Grouping: &values}
			},
		},
	}
	requireEveryReportCollectionBounded(t, tests)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	for _, test := range tests {
		t.Run(test.path, func(t *testing.T) {
			request := Request{CommandID: "report-update-1", Operation: UpdateReport, ResourceID: "report-1", Report: test.definition(MaxReportDefinitionEntries)}
			if err := ValidateRequest(ctx, request); err != nil {
				t.Fatalf("maximum report entries rejected: %v", err)
			}
			request.Report = test.definition(MaxReportDefinitionEntries + 1)
			assertReportEntryLimit(t, ValidateRequest(ctx, request))
		})
	}

	columns := make([]string, MaxReportDefinitionEntries/2)
	filters := make([]ReportFilter, MaxReportDefinitionEntries/2)
	grouping := []string{"OwnerId"}
	request := Request{CommandID: "report-update-total-1", Operation: UpdateReport, ResourceID: "report-1", Report: &ReportDefinition{
		Columns: &columns, Filters: &filters, Grouping: &grouping,
	}}
	assertReportEntryLimit(t, ValidateRequest(ctx, request))
}

func requireEveryReportCollectionBounded(t *testing.T, tests []reportCollectionLimitCase) {
	t.Helper()
	covered := make(map[string]struct{}, len(tests))
	for _, test := range tests {
		covered[test.path] = struct{}{}
	}
	typeOfReport := reflect.TypeOf(ReportDefinition{})
	for index := 0; index < typeOfReport.NumField(); index++ {
		field := typeOfReport.Field(index)
		if field.Type.Kind() != reflect.Ptr || field.Type.Elem().Kind() != reflect.Slice {
			continue
		}
		path := typeOfReport.Name() + "." + field.Name
		if _, ok := covered[path]; !ok {
			t.Fatalf("report collection %s has no entry-boundary case", path)
		}
		delete(covered, path)
	}
	for path := range covered {
		t.Fatalf("entry-boundary case %s does not name a report collection", path)
	}
}

func assertReportEntryLimit(t *testing.T, err error) {
	t.Helper()
	var failure *common.Failure
	if !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted ||
		failure.LimitKind != common.LimitOperationCount || failure.Maximum != MaxReportDefinitionEntries {
		t.Fatalf("report entry limit = %#v, want RESOURCE_EXHAUSTED/%s/%d", err, common.LimitOperationCount, MaxReportDefinitionEntries)
	}
}

func TestDefinitionPayloadAndConfigurationAreBoundedAndCanonical(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := validDashboardCreateRequest()
	request.Dashboard.Description = stringPointer(strings.Repeat("x", MaxDefinitionBytes))
	var failure *common.Failure
	if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted || failure.Maximum != MaxDefinitionBytes {
		t.Fatalf("oversized definition = %#v", err)
	}
	components := []DashboardComponent{{Kind: "CHART", Configuration: strings.Repeat("{", MaxDefinitionBytes+1)}}
	request = validDashboardCreateRequest()
	request.Dashboard.Components = &components
	if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted || failure.Maximum != MaxDefinitionBytes {
		t.Fatalf("oversized unparsed component configuration = %#v", err)
	}
	components = []DashboardComponent{{Kind: "CHART", Configuration: `{ "not": "canonical" }`}}
	request = validDashboardCreateRequest()
	request.Dashboard.Components = &components
	if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest {
		t.Fatalf("non-canonical configuration = %#v", err)
	}
}

func TestAnalyticsDefinitionsRejectUnsafeControlText(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	unsafeName := "Pipeline\x00Injected"
	unsafeDescription := "Description\u0001Injected"
	unsafeComponents := []DashboardComponent{{Kind: "CHART", Title: "Title\x00Injected"}}
	for _, request := range []Request{
		{CommandID: "dashboard-update-1", Operation: UpdateDashboard, ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{Name: &unsafeName}},
		{CommandID: "dashboard-update-1", Operation: UpdateDashboard, ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{Description: &unsafeDescription}},
		{CommandID: "dashboard-update-1", Operation: UpdateDashboard, ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{Components: &unsafeComponents}},
		{CommandID: "folder-update-1", Operation: UpdateFolder, ResourceID: "folder-1", Folder: &FolderDefinition{StableName: &unsafeName}},
	} {
		var failure *common.Failure
		if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest {
			t.Fatalf("ValidateRequest(%#v) = %#v", request, err)
		}
	}
	description := "First line\nSecond line"
	valid := validDashboardCreateRequest()
	valid.Dashboard.Description = &description
	if err := ValidateRequest(ctx, valid); err != nil {
		t.Fatalf("multiline description rejected: %v", err)
	}
}

func TestResponseIsBoundToOneValidRequestOperation(t *testing.T) {
	receipts := Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName, CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1}
	create := validDashboardCreateRequest()
	updateFolder := Request{CommandID: "command-1", Operation: UpdateFolder, ResourceID: "folder-1", Folder: &FolderDefinition{Name: stringPointer("Changed")}}
	updateDashboard := Request{CommandID: "command-1", Operation: UpdateDashboard, ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{Name: stringPointer("Changed")}}
	for _, test := range []struct {
		name     string
		request  Request
		response Response
		valid    bool
	}{
		{name: "create", request: create, response: Response{CommandID: create.CommandID, ResourceID: "dashboard-1", Receipts: receipts}, valid: true},
		{name: "update", request: updateFolder, response: Response{CommandID: "command-1", ResourceID: "folder-1", Receipts: receipts}, valid: true},
		{name: "update dashboard", request: updateDashboard, response: Response{CommandID: "command-1", ResourceID: "dashboard-1", Receipts: receipts}, valid: true},
		{name: "different command", request: create, response: Response{CommandID: "command-2", ResourceID: "dashboard-1", Receipts: receipts}},
		{name: "unsupported operation", request: Request{CommandID: "command-1", Operation: Operation("EXPORT_DASHBOARD"), ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{Name: stringPointer("Changed")}}, response: Response{CommandID: "command-1", ResourceID: "dashboard-1", Receipts: receipts}},
		{name: "different updated resource", request: updateFolder, response: Response{CommandID: "command-1", ResourceID: "folder-2", Receipts: receipts}},
		{name: "unsafe created resource", request: create, response: Response{CommandID: create.CommandID, ResourceID: " dashboard-1", Receipts: receipts}},
	} {
		t.Run(test.name, func(t *testing.T) {
			err := ValidateResponse(test.request, test.response)
			if test.valid && err != nil {
				t.Fatal(err)
			}
			if !test.valid {
				var failure *common.Failure
				if !errors.As(err, &failure) || failure.Category != common.FailureInternal || failure.InvalidComponent != common.InvalidComponentResponse {
					t.Fatalf("ValidateResponse() = %#v, want internal response failure", err)
				}
			}
		})
	}
}

func TestCommandDigestBindsBusinessEffectNotRetryFence(t *testing.T) {
	firstRevision, secondRevision := uint64(1), uint64(2)
	first := validDashboardCreateRequest()
	first.MinCatalogRevision = &firstRevision
	second := first
	second.CommandID = "command-2"
	second.MinCatalogRevision = &secondRevision
	firstDigest, err := CommandDigest(first)
	if err != nil {
		t.Fatal(err)
	}
	if !regexp.MustCompile(`^sha256:[0-9a-f]{64}$`).MatchString(firstDigest) {
		t.Fatalf("CommandDigest() = %q, want canonical sha256 digest spelling", firstDigest)
	}
	secondDigest, err := CommandDigest(second)
	if err != nil {
		t.Fatal(err)
	}
	if firstDigest != secondDigest {
		t.Fatalf("retry-only fields changed business-effect digest: %q != %q", firstDigest, secondDigest)
	}
	changed := *second.Dashboard
	changed.Name = stringPointer("Changed")
	second.Dashboard = &changed
	if digest, err := CommandDigest(second); err != nil || digest == firstDigest {
		t.Fatalf("changed effect digest = %q, %v", digest, err)
	}
}

type optionalCollectionDigestCase struct {
	path     string
	base     func() Request
	setNil   func(*Request)
	setEmpty func(*Request)
}

func TestCommandDigestCanonicalizesEveryOptionalCollection(t *testing.T) {
	tests := []optionalCollectionDigestCase{
		{
			path: "DashboardDefinition.Components",
			base: validDashboardUpdateRequest,
			setNil: func(request *Request) {
				var values []DashboardComponent
				request.Dashboard.Components = &values
			},
			setEmpty: func(request *Request) {
				values := []DashboardComponent{}
				request.Dashboard.Components = &values
			},
		},
		{
			path: "ReportDefinition.Columns",
			base: validReportUpdateRequest,
			setNil: func(request *Request) {
				var values []string
				request.Report.Columns = &values
			},
			setEmpty: func(request *Request) {
				values := []string{}
				request.Report.Columns = &values
			},
		},
		{
			path: "ReportDefinition.Filters",
			base: validReportUpdateRequest,
			setNil: func(request *Request) {
				var values []ReportFilter
				request.Report.Filters = &values
			},
			setEmpty: func(request *Request) {
				values := []ReportFilter{}
				request.Report.Filters = &values
			},
		},
		{
			path: "ReportDefinition.Grouping",
			base: validReportUpdateRequest,
			setNil: func(request *Request) {
				var values []string
				request.Report.Grouping = &values
			},
			setEmpty: func(request *Request) {
				values := []string{}
				request.Report.Grouping = &values
			},
		},
	}
	requireEveryOptionalCollectionCovered(t, tests)
	for _, test := range tests {
		t.Run(test.path, func(t *testing.T) {
			absent := test.base()
			presentNil := test.base()
			test.setNil(&presentNil)
			presentEmpty := test.base()
			test.setEmpty(&presentEmpty)

			nilDigest, err := CommandDigest(presentNil)
			if err != nil {
				t.Fatal(err)
			}
			emptyDigest, err := CommandDigest(presentEmpty)
			if err != nil {
				t.Fatal(err)
			}
			if nilDigest != emptyDigest {
				t.Fatalf("equivalent present empty collections changed digest: %q != %q", nilDigest, emptyDigest)
			}
			absentDigest, err := CommandDigest(absent)
			if err != nil {
				t.Fatal(err)
			}
			if absentDigest == emptyDigest {
				t.Fatal("absent patch and present empty collection share a digest")
			}
		})
	}
}

func requireEveryOptionalCollectionCovered(t *testing.T, tests []optionalCollectionDigestCase) {
	t.Helper()
	covered := make(map[string]struct{}, len(tests))
	for _, test := range tests {
		if _, duplicate := covered[test.path]; duplicate {
			t.Fatalf("duplicate optional collection case %q", test.path)
		}
		covered[test.path] = struct{}{}
	}
	for _, definition := range []reflect.Type{
		reflect.TypeOf(DashboardDefinition{}),
		reflect.TypeOf(ReportDefinition{}),
		reflect.TypeOf(FolderDefinition{}),
	} {
		for index := 0; index < definition.NumField(); index++ {
			field := definition.Field(index)
			if field.Type.Kind() != reflect.Ptr || field.Type.Elem().Kind() != reflect.Slice {
				continue
			}
			path := definition.Name() + "." + field.Name
			if _, ok := covered[path]; !ok {
				t.Fatalf("optional collection %s has no canonical digest case", path)
			}
			delete(covered, path)
		}
	}
	for path := range covered {
		t.Fatalf("canonical digest case %s does not name an optional collection", path)
	}
}

func validDashboardUpdateRequest() Request {
	return Request{CommandID: "dashboard-update-1", Operation: UpdateDashboard, ResourceID: "dashboard-1", Dashboard: &DashboardDefinition{Name: stringPointer("Changed")}}
}

func validReportUpdateRequest() Request {
	return Request{CommandID: "report-update-1", Operation: UpdateReport, ResourceID: "report-1", Report: &ReportDefinition{Name: stringPointer("Changed")}}
}

func validDashboardCreateRequest() Request {
	components := []DashboardComponent{{ReportID: "report-1", Kind: "CHART", Title: "Open", Configuration: `{"color":"blue"}`}}
	return Request{CommandID: "dashboard-create-1", Operation: CreateDashboard, Dashboard: &DashboardDefinition{
		Name: stringPointer("Pipeline"), StableName: stringPointer("Pipeline"), FolderID: stringPointer("folder-1"), LayoutKind: stringPointer("GRID"), Components: &components,
	}}
}

func stringPointer(value string) *string { return &value }
