package compatibility_test

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"reflect"
	"strings"
	"testing"
	"time"

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordviewv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/envelope"
	commonv1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/common/v1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"google.golang.org/protobuf/proto"
)

type recordViewEnvelopeGolden struct {
	Schema            string `json:"schema"`
	EvidenceRevisions struct {
		NexusCRM string `json:"nexuscrm"`
		AppPacks string `json:"nexuscrm_app_packs"`
		Contract string `json:"contract"`
	} `json:"evidence_revisions"`
	BusinessEnvelope struct {
		SemanticRequestCapBytes     int `json:"semantic_request_cap_bytes"`
		ReadRequestWireBytes        int `json:"read_request_wire_bytes"`
		ReadRequestOneOverWireBytes int `json:"read_request_one_over_wire_bytes"`
		ResolveRequestWireBytes     int `json:"resolve_request_wire_bytes"`
		ReadResponseWireBytes       int `json:"read_response_wire_bytes"`
		MaxAttachmentSources        int `json:"max_attachment_sources"`
		SemanticResponseCapBytes    int `json:"semantic_response_cap_bytes"`
	} `json:"business_envelope"`
	Transport struct {
		ReceiveCapBytes        int    `json:"receive_cap_bytes"`
		MinimumReceiveHeadroom int    `json:"minimum_receive_headroom_bytes"`
		SendCapBytes           int    `json:"send_cap_bytes"`
		SendHeadroomBytes      int    `json:"send_headroom_bytes"`
		Compression            string `json:"compression"`
	} `json:"transport"`
}

func TestRecordViewBusinessAndTransportEnvelope(t *testing.T) {
	read, readOneOver := boundaryRecordViewReadRequests(t)
	resolve := boundaryRecordViewResolveRequest(t)
	response := boundaryRecordViewResponse(t)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := semantic.ValidateReadRequest(ctx, read); err != nil {
		t.Fatalf("exact read request rejected: %v", err)
	}
	var oneOverFailure *semantic.Failure
	if err := semantic.ValidateReadRequest(ctx, readOneOver); !errors.As(err, &oneOverFailure) || oneOverFailure.LimitKind != semantic.LimitSemanticRequest {
		t.Fatalf("one-over read request = %#v", err)
	}
	if err := semantic.ValidateResolveRequest(ctx, resolve); err != nil {
		t.Fatalf("exact resolve request rejected: %v", err)
	}
	if err := semantic.ValidateReadResponse(read, response); err != nil {
		t.Fatalf("exact read response rejected: %v", err)
	}
	wireRead := wireRecordViewReadRequest(read)
	wireReadOneOver := wireRecordViewReadRequest(readOneOver)
	wireResolve := wireRecordViewResolveRequest(resolve)
	wireResponse := wireRecordViewResponse(t, read, response)

	var got recordViewEnvelopeGolden
	got.Schema = "runtime-record-view-wire-envelope.v1"
	got.EvidenceRevisions.NexusCRM = "84ceee21c92e240e1f868159ff4d5c585b8f3de6"
	got.EvidenceRevisions.AppPacks = "bd5529eddd5b5eb809d8a7e488eab27b2a9e4e15"
	got.EvidenceRevisions.Contract = "d2dfd70aaad2454e49dbc5e65e52e2be906e1523"
	got.BusinessEnvelope.SemanticRequestCapBytes = semantic.MaxSemanticRequestBytes
	got.BusinessEnvelope.ReadRequestWireBytes = proto.Size(wireRead)
	got.BusinessEnvelope.ReadRequestOneOverWireBytes = proto.Size(wireReadOneOver)
	got.BusinessEnvelope.ResolveRequestWireBytes = proto.Size(wireResolve)
	got.BusinessEnvelope.ReadResponseWireBytes = proto.Size(wireResponse)
	got.BusinessEnvelope.MaxAttachmentSources = semantic.MaxAttachmentSources
	got.BusinessEnvelope.SemanticResponseCapBytes = semantic.MaxSemanticResponseBytes
	got.Transport.ReceiveCapBytes = envelope.MaxRequestWireBytes
	maxRequest := max(got.BusinessEnvelope.ReadRequestWireBytes, got.BusinessEnvelope.ResolveRequestWireBytes)
	got.Transport.MinimumReceiveHeadroom = got.Transport.ReceiveCapBytes - maxRequest
	got.Transport.SendCapBytes = envelope.MaxResponseWireBytes
	got.Transport.SendHeadroomBytes = got.Transport.SendCapBytes - got.BusinessEnvelope.ReadResponseWireBytes
	got.Transport.Compression = "disabled"
	if got.Transport.MinimumReceiveHeadroom <= 0 || got.Transport.SendHeadroomBytes <= 0 {
		t.Fatalf("record-view wire shape exceeds transport envelope: %#v", got.Transport)
	}

	data, err := os.ReadFile("../testdata/record_view_envelope_golden.json")
	if err != nil {
		t.Fatal(err)
	}
	var want recordViewEnvelopeGolden
	if err := json.Unmarshal(data, &want); err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(got, want) {
		encoded, _ := json.MarshalIndent(got, "", "  ")
		t.Fatalf("record-view envelope changed; review deliberately:\n%s", encoded)
	}
}

func boundaryRecordViewReadRequests(t *testing.T) (semantic.ReadRequest, semantic.ReadRequest) {
	t.Helper()
	request := semantic.ReadRequest{
		ObjectID:   strings.Repeat("O", semantic.MaxObjectIdentifierBytes),
		Selector:   semantic.Selector{Kind: semantic.SelectorFieldEqual, FieldID: strings.Repeat("F", semantic.MaxFieldIdentifierBytes), FieldValue: "x"},
		Projection: semantic.ProjectionRoot,
	}
	size, err := semantic.SemanticReadRequestSize(request)
	if err != nil {
		t.Fatal(err)
	}
	request.Selector.FieldValue += strings.Repeat("x", semantic.MaxSemanticRequestBytes-size)
	size, err = semantic.SemanticReadRequestSize(request)
	if err != nil || size != semantic.MaxSemanticRequestBytes {
		t.Fatalf("read semantic boundary = %d, %v", size, err)
	}
	oneOver := request
	oneOver.Selector.FieldValue += "x"
	return request, oneOver
}

func boundaryRecordViewResolveRequest(t *testing.T) semantic.ResolveAttachmentSourcesRequest {
	t.Helper()
	request := semantic.ResolveAttachmentSourcesRequest{
		ParentObjectID: strings.Repeat("O", semantic.MaxObjectIdentifierBytes), ParentRecordID: strings.Repeat("P", semantic.MaxRecordIdentifierBytes),
		ResolverID: strings.Repeat("R", semantic.MaxResolverIdentifierBytes), SourceValues: make([]string, semantic.MaxAttachmentSources),
	}
	for index := range request.SourceValues {
		request.SourceValues[index] = "x"
	}
	size, err := semantic.SemanticResolveRequestSize(request)
	if err != nil {
		t.Fatal(err)
	}
	request.SourceValues[0] += strings.Repeat("x", semantic.MaxSemanticRequestBytes-size)
	size, err = semantic.SemanticResolveRequestSize(request)
	if err != nil || size != semantic.MaxSemanticRequestBytes {
		t.Fatalf("resolve semantic boundary = %d, %v", size, err)
	}
	return request
}

func boundaryRecordViewResponse(t *testing.T) semantic.ReadResponse {
	t.Helper()
	recordID, payload := strings.Repeat("I", semantic.MaxRecordIdentifierBytes), "x"
	response := semantic.ReadResponse{
		RecordID: recordID,
		Root:     semantic.Record{ObjectID: strings.Repeat("O", semantic.MaxObjectIdentifierBytes), Fields: []semantic.Field{{Name: "Payload", Value: semantic.Value{Kind: semantic.ValueString, String: &payload}}}},
		Receipts: semantic.Receipts{ProtocolVersion: "1.0", CatalogRevision: ^uint64(0), SchemaGeneration: ^uint64(0), EngineGeneration: ^uint64(0), SecurityEpoch: ^uint64(0), CapabilityVersion: semantic.CapabilityName},
	}
	size, err := semantic.SemanticReadResponseSize(response)
	if err != nil {
		t.Fatal(err)
	}
	payload += strings.Repeat("x", semantic.MaxSemanticResponseBytes-size)
	response.Root.Fields[0].Value.String = &payload
	size, err = semantic.SemanticReadResponseSize(response)
	if err != nil || size != semantic.MaxSemanticResponseBytes {
		t.Fatalf("response semantic boundary = %d, %v", size, err)
	}
	return response
}

func wireRecordViewReadRequest(request semantic.ReadRequest) *runtimev1.ReadRecordViewRequest {
	return &runtimev1.ReadRecordViewRequest{
		ObjectId:   request.ObjectID,
		Selector:   &runtimev1.RecordViewSelector{Kind: runtimev1.RecordViewSelectorKind_RECORD_VIEW_SELECTOR_KIND_FIELD_EQUAL, FieldId: request.Selector.FieldID, FieldValue: request.Selector.FieldValue},
		Projection: runtimev1.RecordViewProjection_RECORD_VIEW_PROJECTION_ROOT,
	}
}

func wireRecordViewResolveRequest(request semantic.ResolveAttachmentSourcesRequest) *runtimev1.ResolveRecordAttachmentSourcesRequest {
	return &runtimev1.ResolveRecordAttachmentSourcesRequest{
		ParentObjectId: request.ParentObjectID, ParentRecordId: request.ParentRecordID,
		ResolverId: request.ResolverID, SourceValues: append([]string(nil), request.SourceValues...),
	}
}

func wireRecordViewResponse(t *testing.T, request semantic.ReadRequest, response semantic.ReadResponse) *runtimev1.ReadRecordViewResponse {
	t.Helper()
	value := *response.Root.Fields[0].Value.String
	return &runtimev1.ReadRecordViewResponse{
		RecordId: response.RecordID,
		Root:     &runtimev1.QueryRecord{ObjectId: response.Root.ObjectID, Fields: []*runtimev1.QueryField{{Name: "Payload", Value: &runtimev1.QueryValue{DeclaredKind: runtimev1.QueryValueKind_QUERY_VALUE_KIND_STRING, Value: &runtimev1.QueryValue_StringValue{StringValue: value}}}}},
		Receipts: &commonv1.ConsistencyReceipts{
			ProtocolVersion: &commonv1.ProtocolVersion{Major: semantic.ProtocolMajor, Minor: semantic.ProtocolMinor},
			CatalogRevision: response.Receipts.CatalogRevision, SchemaGeneration: response.Receipts.SchemaGeneration,
			EngineGeneration: response.Receipts.EngineGeneration, SecurityEpoch: response.Receipts.SecurityEpoch,
			CapabilityVersion: &commonv1.CapabilityVersion{Name: semantic.CapabilityName, Major: 1},
		},
	}
}
