package mapping

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"testing"
	"time"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	v1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	v2 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv2"
	history "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"google.golang.org/protobuf/proto"
)

func historyAdmissionRequest(count int) v2.Request {
	request := v2.Request{ObjectID: "Issue", ExpectedLayoutRevision: layoutAuthoringV2RevisionA,
		Operation: v2.Operation{SetDefault: &v2.SetDefaultLayout{LayoutID: "layout-main"}}}
	for i := 0; i < count; i++ {
		request.HistoryRelatedLists = append(request.HistoryRelatedLists, v2.HistoryRelatedListProjection{ObjectID: fmt.Sprintf("Changes%d", i), Columns: map[string]history.HistoryColumn{
			"Key": history.HistoryColumnID, "Parent": history.HistoryColumnParentID, "Field": history.HistoryColumnField,
			"Before": history.HistoryColumnOldValue, "After": history.HistoryColumnNewValue, "Type": history.HistoryColumnDataType,
			"Time": history.HistoryColumnCreatedDate, "Actor": history.HistoryColumnCreatedByID,
		}})
	}
	return request
}

func TestLayoutAuthoringHistoryAdmissionContextPrecedence(t *testing.T) {
	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	expired, expire := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
	defer expire()
	contexts := []struct {
		name     string
		ctx      context.Context
		category common.FailureCategory
		invalid  common.InvalidComponent
	}{
		{"nil", nil, common.FailureInvalidRequest, common.InvalidComponentDeadline},
		{"no deadline", context.Background(), common.FailureInvalidRequest, common.InvalidComponentDeadline},
		{"canceled", canceled, common.FailureCanceled, ""},
		{"expired", expired, common.FailureDeadlineExceeded, ""},
	}
	base, err := ToWireLayoutAuthoringV2MutateRequest(layoutAuthoringTestContext(t), historyAdmissionRequest(1))
	if err != nil {
		t.Fatal(err)
	}
	for _, payload := range []struct {
		name string
		edit func(*runtimev1.LayoutAuthoringV2ServiceMutateRequest)
	}{
		{"valid", func(r *runtimev1.LayoutAuthoringV2ServiceMutateRequest) {}},
		{"excess projections", func(r *runtimev1.LayoutAuthoringV2ServiceMutateRequest) {
			r.HistoryRelatedLists = make([]*runtimev1.LayoutAuthoringHistoryRelatedListProjection, v2.MaxHistoryRelatedListProjections+1)
		}},
		{"excess columns", func(r *runtimev1.LayoutAuthoringV2ServiceMutateRequest) {
			r.HistoryRelatedLists[0].Columns["Extra"] = "id"
		}},
		{"nil projection", func(r *runtimev1.LayoutAuthoringV2ServiceMutateRequest) { r.HistoryRelatedLists[0] = nil }},
		{"unknown member", func(r *runtimev1.LayoutAuthoringV2ServiceMutateRequest) {
			r.HistoryRelatedLists[0].Columns["Key"] = "unknown"
		}},
		{"create document limit", func(r *runtimev1.LayoutAuthoringV2ServiceMutateRequest) {
			r.Operation = &runtimev1.LayoutAuthoringV2ServiceMutateRequest_Create{Create: &runtimev1.LayoutAuthoringV2Create{Document: &runtimev1.LayoutAuthoringDocument{CompactFields: make([]string, v1.MaxCompactFields+1)}}}
		}},
		{"update field limit", func(r *runtimev1.LayoutAuthoringV2ServiceMutateRequest) {
			r.Operation = &runtimev1.LayoutAuthoringV2ServiceMutateRequest_Update{Update: &runtimev1.LayoutAuthoringV2Update{Changes: &runtimev1.LayoutAuthoringDocumentChanges{}, ListFields: &runtimev1.LayoutAuthoringStringListChange{Value: &runtimev1.LayoutAuthoringStringList{Values: make([]string, v1.MaxListFields+1)}}}}
		}},
	} {
		for _, cc := range contexts {
			t.Run(payload.name+"/"+cc.name, func(t *testing.T) {
				request := proto.Clone(base).(*runtimev1.LayoutAuthoringV2ServiceMutateRequest)
				payload.edit(request)
				_, err := FromWireLayoutAuthoringV2MutateRequest(cc.ctx, request)
				assertPlatformFailure(t, err, cc.category, cc.invalid)
				assertPlatformFailure(t, FromPlatformStatusError(ToPlatformStatusError(err, v2.CapabilityName), v2.CapabilityName), cc.category, cc.invalid)
			})
		}
	}
	for _, cc := range contexts {
		t.Run("semantic/"+cc.name, func(t *testing.T) {
			_, err := ToWireLayoutAuthoringV2MutateRequest(cc.ctx, historyAdmissionRequest(v2.MaxHistoryRelatedListProjections+1))
			assertPlatformFailure(t, err, cc.category, cc.invalid)
		})
	}
}

func TestLayoutAuthoringHistoryAdmissionRoundTripBoundaries(t *testing.T) {
	for _, count := range []int{0, 1, v2.MaxHistoryRelatedListProjections - 1, v2.MaxHistoryRelatedListProjections} {
		t.Run(fmt.Sprint(count), func(t *testing.T) {
			request := historyAdmissionRequest(count)
			wire, err := ToWireLayoutAuthoringV2MutateRequest(layoutAuthoringTestContext(t), request)
			if err != nil {
				t.Fatal(err)
			}
			data, err := proto.Marshal(wire)
			if err != nil {
				t.Fatal(err)
			}
			decoded := &runtimev1.LayoutAuthoringV2ServiceMutateRequest{}
			if err := PreflightPlatformWirePayload(data, decoded); err != nil {
				t.Fatal(err)
			}
			if err := proto.Unmarshal(data, decoded); err != nil {
				t.Fatal(err)
			}
			actual, err := FromWireLayoutAuthoringV2MutateRequest(layoutAuthoringTestContext(t), decoded)
			if err != nil || !reflect.DeepEqual(request, actual) {
				t.Fatalf("round trip: %v, equal=%v", err, reflect.DeepEqual(request, actual))
			}
		})
	}
}

func TestLayoutAuthoringHistoryAdmissionRejectsInvalidBindings(t *testing.T) {
	for _, tc := range []struct {
		name string
		edit func(*v2.Request)
	}{
		{"unknown member", func(r *v2.Request) { r.HistoryRelatedLists[0].Columns["Key"] = "unknown" }},
		{"missing member", func(r *v2.Request) { delete(r.HistoryRelatedLists[0].Columns, "Key") }},
		{"duplicate member", func(r *v2.Request) { r.HistoryRelatedLists[0].Columns["Key"] = history.HistoryColumnField }},
		{"case folded alias", func(r *v2.Request) {
			delete(r.HistoryRelatedLists[0].Columns, "Key")
			r.HistoryRelatedLists[0].Columns["FIELD"] = history.HistoryColumnID
		}},
		{"parent shadow", func(r *v2.Request) { r.HistoryRelatedLists[0].ObjectID = "issue" }},
		{"duplicate projection", func(r *v2.Request) { r.HistoryRelatedLists = append(r.HistoryRelatedLists, r.HistoryRelatedLists[0]) }},
	} {
		t.Run(tc.name, func(t *testing.T) {
			request := historyAdmissionRequest(1)
			tc.edit(&request)
			_, err := ToWireLayoutAuthoringV2MutateRequest(layoutAuthoringTestContext(t), request)
			assertPlatformFailure(t, err, common.FailureInvalidRequest, common.InvalidComponentCommandData)
			wire := &runtimev1.LayoutAuthoringV2ServiceMutateRequest{ObjectId: request.ObjectID, ExpectedLayoutRevision: request.ExpectedLayoutRevision, Operation: &runtimev1.LayoutAuthoringV2ServiceMutateRequest_SetDefault{SetDefault: &runtimev1.LayoutAuthoringV2SetDefault{LayoutId: "layout-main"}}}
			for _, p := range request.HistoryRelatedLists {
				columns := map[string]string{}
				for name, member := range p.Columns {
					columns[name] = string(member)
				}
				wire.HistoryRelatedLists = append(wire.HistoryRelatedLists, &runtimev1.LayoutAuthoringHistoryRelatedListProjection{ObjectId: p.ObjectID, Columns: columns})
			}
			_, err = FromWireLayoutAuthoringV2MutateRequest(layoutAuthoringTestContext(t), wire)
			// Malformed wire shape and invalid semantic bindings share the public invalid-request category.
			var failure *common.Failure
			if !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest {
				t.Fatalf("invalid binding: %v", err)
			}
		})
	}
}
