package compatibility_test

import (
	"context"
	"errors"
	"net"
	"strings"
	"sync/atomic"
	"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"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"
	"google.golang.org/grpc/test/bufconn"
)

func TestGeneratedApplicationResultClientReadsPendingAndTerminalFacts(t *testing.T) {
	client, service, cleanup := startApplicationResultTestServer(t)
	defer cleanup()
	ctx, cancel := context.WithTimeout(withCapability(context.Background(), semantic.CapabilityName), time.Second)
	defer cancel()
	digest := "sha256:" + strings.Repeat("1", 64)

	pending, err := client.GetApplicationResult(ctx, &runtimev1.GetApplicationResultRequest{
		CommandId: "pending-command", ExecutionRefDigest: digest,
	})
	if err != nil || pending.GetState() != runtimev1.ApplicationResultState_APPLICATION_RESULT_STATE_PENDING ||
		pending.GetResult() != nil {
		t.Fatalf("pending response = %#v, error = %v", pending, err)
	}

	terminal, err := client.GetApplicationResult(ctx, &runtimev1.GetApplicationResultRequest{
		CommandId: "terminal-command", ExecutionRefDigest: digest,
	})
	if err != nil || terminal.GetState() != runtimev1.ApplicationResultState_APPLICATION_RESULT_STATE_TERMINAL ||
		terminal.GetResult().GetCommandId() != "terminal-command" ||
		terminal.GetResult().GetExecutionRefDigest() != digest ||
		terminal.GetResult().GetOutcome() != runtimev1.ApplicationResultOutcome_APPLICATION_RESULT_OUTCOME_APPLIED ||
		service.calls.Load() != 2 {
		t.Fatalf("terminal response = %#v, error = %v, calls = %d", terminal, err, service.calls.Load())
	}
}

func TestGeneratedApplicationResultClientFailsClosedBeforeHandlerOnCapabilityMismatch(t *testing.T) {
	client, service, cleanup := startApplicationResultTestServer(t)
	defer cleanup()
	ctx, cancel := context.WithTimeout(withProtocol(context.Background()), time.Second)
	defer cancel()

	response, err := client.GetApplicationResult(ctx, &runtimev1.GetApplicationResultRequest{})
	if response != nil || status.Code(err) != codes.FailedPrecondition || service.calls.Load() != 0 {
		t.Fatalf("response = %#v, error = %v, calls = %d", response, err, service.calls.Load())
	}
	var failure *commonsemantic.Failure
	if decoded := mapping.FromPlatformStatusError(err, semantic.CapabilityName); !errors.As(decoded, &failure) ||
		failure.Category != commonsemantic.FailureIncompatible {
		t.Fatalf("decoded capability mismatch = %#v", decoded)
	}
}

func TestApplicationResultServiceIsAdditiveAgainstCommandOnlyEndpoint(t *testing.T) {
	listener := bufconn.Listen(1024 * 1024)
	server := grpc.NewServer()
	runtimev1.RegisterCommandAcceptanceServiceServer(server, &commandOnlyServer{})
	go func() { _ = server.Serve(listener) }()
	t.Cleanup(func() {
		server.Stop()
		_ = listener.Close()
	})

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	connection, err := grpc.DialContext(
		ctx,
		"passthrough:///bufnet",
		grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		t.Fatal(err)
	}
	defer connection.Close()

	accepted, err := runtimev1.NewCommandAcceptanceServiceClient(connection).AcceptCommand(
		ctx, &runtimev1.AcceptCommandRequest{CommandId: "command-1"},
	)
	if err != nil || accepted.GetCommandId() != "command-1" {
		t.Fatalf("existing command service response = %#v, error = %v", accepted, err)
	}
	result, err := runtimev1.NewApplicationResultServiceClient(connection).GetApplicationResult(
		ctx, &runtimev1.GetApplicationResultRequest{},
	)
	if result != nil || status.Code(err) != codes.Unimplemented {
		t.Fatalf("unregistered additive service response = %#v, error = %v", result, err)
	}
}

type commandOnlyServer struct {
	runtimev1.UnimplementedCommandAcceptanceServiceServer
}

func (*commandOnlyServer) AcceptCommand(_ context.Context, request *runtimev1.AcceptCommandRequest) (*runtimev1.AcceptCommandResponse, error) {
	return &runtimev1.AcceptCommandResponse{CommandId: request.GetCommandId()}, nil
}

type testApplicationResultService struct {
	runtimev1.UnimplementedApplicationResultServiceServer
	calls atomic.Int64
}

func (service *testApplicationResultService) GetApplicationResult(ctx context.Context, request *runtimev1.GetApplicationResultRequest) (*runtimev1.GetApplicationResultResponse, error) {
	service.calls.Add(1)
	semanticRequest, err := mapping.FromWireGetApplicationResultRequest(ctx, request)
	if err != nil {
		return nil, mapping.ToPlatformStatusError(err, semantic.CapabilityName)
	}
	response := semantic.GetResultResponse{
		CommandID: semanticRequest.CommandID, ExecutionRefDigest: semanticRequest.ExecutionRefDigest,
		State: semantic.ResultPending,
	}
	if semanticRequest.CommandID == "terminal-command" {
		result := semantic.ApplicationResult{
			CommandID: semanticRequest.CommandID, ExecutionRefDigest: semanticRequest.ExecutionRefDigest,
			Outcome: semantic.OutcomeApplied, ResultDigest: "sha256:" + strings.Repeat("2", 64),
		}
		result.RuntimeCommitReceipt, err = semantic.ComputeRuntimeCommitReceipt(result)
		if err != nil {
			return nil, mapping.ToPlatformStatusError(err, semantic.CapabilityName)
		}
		response.State = semantic.ResultTerminal
		response.Result = &result
	}
	wireResponse, err := mapping.ToWireGetApplicationResultResponse(semanticRequest, response)
	if err != nil {
		return nil, mapping.ToPlatformStatusError(err, semantic.CapabilityName)
	}
	return wireResponse, nil
}

func startApplicationResultTestServer(t *testing.T) (runtimev1.ApplicationResultServiceClient, *testApplicationResultService, func()) {
	t.Helper()
	service := &testApplicationResultService{}
	connection, cleanup := startBufconnServer(t, func(server *grpc.Server) {
		runtimev1.RegisterApplicationResultServiceServer(server, service)
	})
	return runtimev1.NewApplicationResultServiceClient(connection), service, cleanup
}
