package mapping

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

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationresultv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protowire"
	"google.golang.org/protobuf/proto"
)

func applicationResultDeadlineContext(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	return ctx
}

func TestApplicationResultMappingRoundTripsPendingAndEveryTerminalOutcome(t *testing.T) {
	ctx := applicationResultDeadlineContext(t)
	request := applicationResultRequest()
	wireRequest, err := ToWireGetApplicationResultRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	roundRequest, err := FromWireGetApplicationResultRequest(ctx, wireRequest)
	if err != nil || roundRequest != request {
		t.Fatalf("request round trip = %#v, %v", roundRequest, err)
	}

	pending := semantic.GetResultResponse{
		CommandID: request.CommandID, ExecutionRefDigest: request.ExecutionRefDigest,
		State: semantic.ResultPending,
	}
	wirePending, err := ToWireGetApplicationResultResponse(request, pending)
	if err != nil {
		t.Fatal(err)
	}
	if wirePending.GetState() != runtimev1.ApplicationResultState_APPLICATION_RESULT_STATE_PENDING || wirePending.GetResult() != nil {
		t.Fatalf("pending wire response = %#v", wirePending)
	}
	roundPending, err := FromWireGetApplicationResultResponse(request, wirePending)
	if err != nil || !reflect.DeepEqual(roundPending, pending) {
		t.Fatalf("pending round trip = %#v, %v", roundPending, err)
	}

	for _, test := range []struct {
		name    string
		outcome semantic.Outcome
		failure semantic.FailureCode
	}{
		{name: "applied", outcome: semantic.OutcomeApplied, failure: semantic.FailureNone},
		{name: "noop", outcome: semantic.OutcomeNoop, failure: semantic.FailureNone},
		{name: "rejected", outcome: semantic.OutcomeRejected, failure: semantic.FailureApplicationRejected},
		{name: "failed", outcome: semantic.OutcomeFailed, failure: semantic.FailureRuntimeExecutionFailed},
	} {
		t.Run(test.name, func(t *testing.T) {
			applicationResult := validMappedApplicationResult(t, test.outcome, test.failure)
			response := semantic.GetResultResponse{
				CommandID: request.CommandID, ExecutionRefDigest: request.ExecutionRefDigest,
				State: semantic.ResultTerminal, Result: &applicationResult,
			}
			wireResponse, err := ToWireGetApplicationResultResponse(request, response)
			if err != nil {
				t.Fatal(err)
			}
			if wireResponse.GetState() != runtimev1.ApplicationResultState_APPLICATION_RESULT_STATE_TERMINAL || wireResponse.GetResult() == nil {
				t.Fatalf("terminal wire response = %#v", wireResponse)
			}
			roundResponse, err := FromWireGetApplicationResultResponse(request, wireResponse)
			if err != nil || !reflect.DeepEqual(roundResponse, response) {
				t.Fatalf("terminal round trip = %#v, %v", roundResponse, err)
			}
		})
	}
}

func TestApplicationResultMapperUsesSemanticRequestValidation(t *testing.T) {
	request := applicationResultRequest()
	if wireRequest, err := ToWireGetApplicationResultRequest(context.Background(), request); err == nil || wireRequest != nil {
		t.Fatalf("request without deadline = %#v, %v", wireRequest, err)
	}

	ctx := applicationResultDeadlineContext(t)
	var failure *commonsemantic.Failure
	if decoded, err := FromWireGetApplicationResultRequest(ctx, nil); err == nil || decoded != (semantic.GetResultRequest{}) ||
		!errors.As(err, &failure) || failure.Category != commonsemantic.FailureInvalidRequest ||
		failure.InvalidComponent != commonsemantic.InvalidComponentCommandData {
		t.Fatalf("nil request = %#v, %#v", decoded, err)
	}

	invalid := &runtimev1.GetApplicationResultRequest{CommandId: "command-1", ExecutionRefDigest: "not-a-digest"}
	if decoded, err := FromWireGetApplicationResultRequest(ctx, invalid); err == nil || decoded != (semantic.GetResultRequest{}) {
		t.Fatalf("invalid request = %#v, %#v", decoded, err)
	}
}

func TestApplicationResultMapperRejectsEveryAmbiguousWireResponse(t *testing.T) {
	request := applicationResultRequest()
	for name, mutate := range map[string]func(*runtimev1.GetApplicationResultResponse){
		"unspecified state": func(response *runtimev1.GetApplicationResultResponse) {
			response.State = runtimev1.ApplicationResultState_APPLICATION_RESULT_STATE_UNSPECIFIED
		},
		"unknown state": func(response *runtimev1.GetApplicationResultResponse) {
			response.State = runtimev1.ApplicationResultState(99)
		},
		"pending with result": func(response *runtimev1.GetApplicationResultResponse) {
			response.State = runtimev1.ApplicationResultState_APPLICATION_RESULT_STATE_PENDING
		},
		"terminal without result": func(response *runtimev1.GetApplicationResultResponse) {
			response.Result = nil
		},
		"foreign response command": func(response *runtimev1.GetApplicationResultResponse) {
			response.CommandId = "command-2"
		},
		"foreign result command": func(response *runtimev1.GetApplicationResultResponse) {
			response.Result.CommandId = "command-2"
		},
		"foreign result execution": func(response *runtimev1.GetApplicationResultResponse) {
			response.Result.ExecutionRefDigest = digestForMappingTest("execution-2")
		},
		"unknown outcome": func(response *runtimev1.GetApplicationResultResponse) {
			response.Result.Outcome = runtimev1.ApplicationResultOutcome(99)
		},
		"wrong failure pairing": func(response *runtimev1.GetApplicationResultResponse) {
			response.Result.Outcome = runtimev1.ApplicationResultOutcome_APPLICATION_RESULT_OUTCOME_REJECTED
		},
		"unknown failure code": func(response *runtimev1.GetApplicationResultResponse) {
			response.Result.FailureCode = runtimev1.ApplicationResultFailureCode(99)
		},
		"forged commit receipt": func(response *runtimev1.GetApplicationResultResponse) {
			response.Result.RuntimeCommitReceipt = digestForMappingTest("forged")
		},
	} {
		t.Run(name, func(t *testing.T) {
			result := validMappedApplicationResult(t, semantic.OutcomeApplied, semantic.FailureNone)
			valid := semantic.GetResultResponse{
				CommandID: request.CommandID, ExecutionRefDigest: request.ExecutionRefDigest,
				State: semantic.ResultTerminal, Result: &result,
			}
			wireResponse, err := ToWireGetApplicationResultResponse(request, valid)
			if err != nil {
				t.Fatal(err)
			}
			mutate(wireResponse)
			decoded, err := FromWireGetApplicationResultResponse(request, wireResponse)
			var failure *commonsemantic.Failure
			if err == nil || decoded != (semantic.GetResultResponse{}) || !errors.As(err, &failure) || failure.Category != commonsemantic.FailureInternal {
				t.Fatalf("decoded response = %#v, error = %#v", decoded, err)
			}
		})
	}

	decoded, err := FromWireGetApplicationResultResponse(request, nil)
	var failure *commonsemantic.Failure
	if err == nil || decoded != (semantic.GetResultResponse{}) || !errors.As(err, &failure) || failure.Category != commonsemantic.FailureInternal {
		t.Fatalf("nil response = %#v, %#v", decoded, err)
	}
}

func TestApplicationResultMapperIgnoresUnknownAdditiveWireField(t *testing.T) {
	request := applicationResultRequest()
	result := validMappedApplicationResult(t, semantic.OutcomeApplied, semantic.FailureNone)
	response := semantic.GetResultResponse{
		CommandID: request.CommandID, ExecutionRefDigest: request.ExecutionRefDigest,
		State: semantic.ResultTerminal, Result: &result,
	}
	wireResponse, err := ToWireGetApplicationResultResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	payload, err := proto.Marshal(wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	payload = protowire.AppendTag(payload, 100, protowire.BytesType)
	payload = protowire.AppendString(payload, "future")
	decodedWire := new(runtimev1.GetApplicationResultResponse)
	if err := proto.Unmarshal(payload, decodedWire); err != nil {
		t.Fatal(err)
	}
	decoded, err := FromWireGetApplicationResultResponse(request, decodedWire)
	if err != nil || !reflect.DeepEqual(decoded, response) {
		t.Fatalf("response with unknown field = %#v, %v", decoded, err)
	}
}

func TestApplicationResultMapperRejectsUnknownSemanticMappingRevision(t *testing.T) {
	var failure *commonsemantic.Failure
	if err := requireApplicationResultSemanticMappingRevisions(struct{}{}); !errors.As(err, &failure) || failure.Category != commonsemantic.FailureIncompatible {
		t.Fatalf("missing semantic mapping revision = %#v", err)
	}
	failure = nil
	if err := requireApplicationResultSemanticMappingRevisions(applicationResultRevisionFixture(supportedApplicationResultSemanticMappingRevision + 1)); !errors.As(err, &failure) || failure.Category != commonsemantic.FailureIncompatible {
		t.Fatalf("future semantic mapping revision = %#v", err)
	}
	if err := requireApplicationResultSemanticMappingRevisions(applicationResultRevisionFixture(supportedApplicationResultSemanticMappingRevision)); err != nil {
		t.Fatalf("supported semantic mapping revision = %v", err)
	}
}

func TestApplicationResultStatusMappingPreservesOpaqueMissing(t *testing.T) {
	wireError := ToPlatformStatusError(
		&commonsemantic.Failure{Category: commonsemantic.FailureMissing},
		semantic.CapabilityName,
	)
	if status.Code(wireError) != codes.NotFound {
		t.Fatalf("missing status = %v", status.Code(wireError))
	}
	var failure *commonsemantic.Failure
	if decoded := FromPlatformStatusError(wireError, semantic.CapabilityName); !errors.As(decoded, &failure) || failure.Category != commonsemantic.FailureMissing {
		t.Fatalf("decoded missing = %#v", decoded)
	}

	invalid := &commonsemantic.Failure{
		Category: commonsemantic.FailureInvalidRequest, InvalidComponent: commonsemantic.InvalidComponentObjectID,
	}
	if got := ToPlatformStatusError(invalid, semantic.CapabilityName); status.Code(got) != codes.Internal {
		t.Fatalf("unsupported detail status = %v", status.Code(got))
	}
}

type applicationResultRevisionFixture uint32

func (revision applicationResultRevisionFixture) ApplicationResultSemanticMappingRevision() uint32 {
	return uint32(revision)
}

func applicationResultRequest() semantic.GetResultRequest {
	return semantic.GetResultRequest{
		CommandID: "command-1", ExecutionRefDigest: digestForMappingTest("execution-1"),
	}
}

func validMappedApplicationResult(t *testing.T, outcome semantic.Outcome, failure semantic.FailureCode) semantic.ApplicationResult {
	t.Helper()
	result := semantic.ApplicationResult{
		CommandID: "command-1", ExecutionRefDigest: digestForMappingTest("execution-1"),
		Outcome: outcome, ResultDigest: digestForMappingTest("result-1"), FailureCode: failure,
	}
	receipt, err := semantic.ComputeRuntimeCommitReceipt(result)
	if err != nil {
		t.Fatal(err)
	}
	result.RuntimeCommitReceipt = receipt
	return result
}

func digestForMappingTest(seed string) string {
	if seed == "execution-1" {
		return "sha256:" + strings.Repeat("1", 64)
	}
	if seed == "result-1" {
		return "sha256:" + strings.Repeat("2", 64)
	}
	return "sha256:" + strings.Repeat("3", 64)
}
