package xerr

import (
	"go/ast"
	"go/parser"
	"go/token"
	"strconv"
	"testing"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// TestToGRPCErrorCarriesReason 锁定服务底座要求的 gRPC code + ErrorInfo.reason 组合。
func TestToGRPCErrorCarriesReason(t *testing.T) {
	err := ToGRPCError(New(PasswordAlreadySet, "password already set"))

	st, ok := status.FromError(err)
	if !ok {
		t.Fatalf("expected grpc status")
	}

	if st.Code() != codes.AlreadyExists {
		t.Fatalf("grpc code mismatch: got %s", st.Code())
	}

	if got := ReasonFromGRPC(err); got != PasswordAlreadySet {
		t.Fatalf("reason mismatch: got %s want %s", got, PasswordAlreadySet)
	}
}

func TestCatalogCoversDeclaredCodes(t *testing.T) {
	declared := declaredCodesFromSource(t)

	for code, name := range declared {
		if _, ok := catalog[code]; !ok {
			t.Fatalf("declared xerr code %s (%s) is missing from catalog", name, code)
		}
	}

	for code := range catalog {
		if _, ok := declared[code]; !ok {
			t.Fatalf("catalog registers undeclared xerr code %s", code)
		}
	}
}

func TestCatalogMappings(t *testing.T) {
	tests := []struct {
		name          string
		code          Code
		grpcCode      codes.Code
		httpStatus    int
		publicCode    string
		publicMessage string
	}{
		{
			name:          "business reason keeps canonical grpc code",
			code:          InvalidInviteCode,
			grpcCode:      codes.InvalidArgument,
			httpStatus:    httpStatusBadRequest,
			publicCode:    string(InvalidInviteCode),
			publicMessage: "invalid argument",
		},
		{
			name:          "unavailable is exposed as upstream error",
			code:          Unavailable,
			grpcCode:      codes.Unavailable,
			httpStatus:    httpStatusBadGateway,
			publicCode:    "UPSTREAM_ERROR",
			publicMessage: "upstream service error",
		},
		{
			name:          "ledger conflict uses aborted grpc but http conflict",
			code:          LedgerConflict,
			grpcCode:      codes.Aborted,
			httpStatus:    httpStatusConflict,
			publicCode:    string(LedgerConflict),
			publicMessage: "conflict",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			spec := SpecOf(test.code)
			if spec.GRPCCode != test.grpcCode ||
				spec.HTTPStatus != test.httpStatus ||
				spec.PublicCode != test.publicCode ||
				spec.PublicMessage != test.publicMessage {
				t.Fatalf("catalog mismatch: got %+v", spec)
			}
		})
	}
}

func declaredCodesFromSource(t *testing.T) map[Code]string {
	t.Helper()

	file, err := parser.ParseFile(token.NewFileSet(), "errors.go", nil, 0)
	if err != nil {
		t.Fatalf("parse errors.go: %v", err)
	}

	declared := make(map[Code]string)
	ast.Inspect(file, func(node ast.Node) bool {
		decl, ok := node.(*ast.GenDecl)
		if !ok || decl.Tok != token.CONST {
			return true
		}

		for _, spec := range decl.Specs {
			valueSpec, ok := spec.(*ast.ValueSpec)
			if !ok {
				continue
			}
			for index, name := range valueSpec.Names {
				if index >= len(valueSpec.Values) {
					continue
				}
				literal, ok := valueSpec.Values[index].(*ast.BasicLit)
				if !ok || literal.Kind != token.STRING {
					continue
				}
				value, err := strconv.Unquote(literal.Value)
				if err != nil {
					t.Fatalf("unquote %s: %v", literal.Value, err)
				}
				declared[Code(value)] = name.Name
			}
		}

		return false
	})

	return declared
}
