package grpc

import (
	"context"
	"net"
	"testing"
	"time"

	activityv1 "hyapp.local/api/proto/activity/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	taskdomain "hyapp/services/activity-service/internal/domain/task"
	taskservice "hyapp/services/activity-service/internal/service/task"
	"hyapp/services/activity-service/internal/testutil/mysqltest"

	grpcpkg "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 TestTaskGRPCInterfaceFlow(t *testing.T) {
	repository := mysqltest.NewRepository(t)
	wallet := &taskFlowWallet{}
	service := taskservice.New(repository, wallet)
	now := time.Date(2026, 5, 9, 10, 0, 0, 0, time.UTC)
	service.SetClock(func() time.Time { return now })

	conn := newTaskFlowGRPCConn(t, service)
	adminClient := activityv1.NewAdminTaskServiceClient(conn)
	appClient := activityv1.NewTaskServiceClient(conn)
	ctx := context.Background()

	// 旧 Admin 二进制仍可能在滚动发布窗口调用已退役 RPC；wire contract 保留，但不能再静默写入 App 级默认资产。
	_, err := adminClient.PublishTaskRewardPolicy(ctx, &activityv1.PublishTaskRewardPolicyRequest{
		Meta:            taskFlowMeta("retired-task-reward-policy"),
		InstanceCode:    "legacy-instance",
		RewardAssetType: taskdomain.RewardAssetPoint,
	})
	if status.Code(err) != codes.FailedPrecondition {
		t.Fatalf("retired task reward policy rpc should fail explicitly: %v", err)
	}

	// 后台真实 gRPC 接口创建一条带 icon、跳转、维度过滤的每日任务，模拟 admin-server 最终调用 activity-service 的配置链路。
	created, err := adminClient.UpsertTaskDefinition(ctx, &activityv1.UpsertTaskDefinitionRequest{
		Meta:                taskFlowMeta("admin-task-create"),
		TaskType:            taskdomain.TypeDaily,
		Category:            "gift",
		MetricType:          taskdomain.MetricGiftSpendCoin,
		Title:               "赠送指定礼物",
		Description:         "消耗指定礼物金币",
		AudienceType:        taskdomain.AudienceNewbie,
		IconKey:             "gift",
		IconUrl:             "https://cdn.example.com/tasks/gift.png",
		ActionType:          taskdomain.ActionGiftPanelSpecific,
		ActionParam:         "gift_1001",
		ActionPayloadJson:   `{"gift_id":"gift_1001","panel":"gift"}`,
		DimensionFilterJson: `{"gift_id":"gift_1001"}`,
		TargetValue:         100,
		TargetUnit:          "coin",
		RewardCoinAmount:    20,
		RewardAssetType:     taskdomain.RewardAssetCoin,
		Status:              taskdomain.StatusActive,
		SortOrder:           10,
		OperatorAdminId:     90001,
	})
	if err != nil {
		t.Fatalf("admin create task failed: %v", err)
	}
	if !created.GetCreated() || created.GetTask().GetTaskId() == "" {
		t.Fatalf("admin create response mismatch: %+v", created)
	}
	taskID := created.GetTask().GetTaskId()

	// 后台列表接口必须能原样读回展示配置，否则 admin-platform 编辑弹窗会丢失 icon、跳转或维度过滤字段。
	adminList, err := adminClient.ListTaskDefinitions(ctx, &activityv1.ListTaskDefinitionsRequest{
		Meta:     taskFlowMeta("admin-task-list"),
		TaskType: taskdomain.TypeDaily,
		Status:   taskdomain.StatusActive,
		Page:     1,
		PageSize: 20,
	})
	if err != nil {
		t.Fatalf("admin list task failed: %v", err)
	}
	if adminList.GetTotal() != 1 || adminList.GetTasks()[0].GetIconKey() != "gift" || adminList.GetTasks()[0].GetIconUrl() == "" || adminList.GetTasks()[0].GetRewardAssetType() != taskdomain.RewardAssetCoin {
		t.Fatalf("admin list should include icon metadata: %+v", adminList)
	}

	// 非匹配 gift_id 的服务端事实事件应被消费为 skipped，证明 dimension_filter_json 生效且 MQ 不会无限重试。
	skipped, err := appClient.ConsumeTaskEvent(ctx, &activityv1.ConsumeTaskEventRequest{
		Meta:           taskFlowMeta("task-event-skip"),
		EventId:        "evt-flow-gift-skip",
		EventType:      "RoomGiftSent",
		SourceService:  "room-service",
		UserId:         10001,
		MetricType:     taskdomain.MetricGiftSpendCoin,
		Value:          100,
		OccurredAtMs:   now.UnixMilli(),
		DimensionsJson: `{"gift_id":"gift_2002","room_id":"room-1"}`,
	})
	if err != nil {
		t.Fatalf("consume skip event failed: %v", err)
	}
	if skipped.GetStatus() != taskdomain.EventStatusSkipped || skipped.GetMatchedTaskCount() != 0 {
		t.Fatalf("skip event result mismatch: %+v", skipped)
	}

	// App 查询接口此时能看到后台配置的图标和跳转，但进度仍为 0，避免维度不匹配事件误推动任务。
	beforeHit := listDailyTaskViaGRPC(t, appClient, 10001, taskID)
	if beforeHit.GetProgressValue() != 0 || beforeHit.GetClaimable() {
		t.Fatalf("task should not progress before matching event: %+v", beforeHit)
	}
	if beforeHit.GetIconKey() != "gift" || beforeHit.GetActionType() != taskdomain.ActionGiftPanelSpecific || beforeHit.GetActionParam() != "gift_1001" {
		t.Fatalf("app task should expose icon/action metadata: %+v", beforeHit)
	}

	// 匹配 gift_id 的服务端事实事件会推进同一条任务到完成态，整个进度来自 activity-service 事件入口。
	consumed, err := appClient.ConsumeTaskEvent(ctx, &activityv1.ConsumeTaskEventRequest{
		Meta:           taskFlowMeta("task-event-hit"),
		EventId:        "evt-flow-gift-hit",
		EventType:      "RoomGiftSent",
		SourceService:  "room-service",
		UserId:         10001,
		MetricType:     taskdomain.MetricGiftSpendCoin,
		Value:          120,
		OccurredAtMs:   now.UnixMilli(),
		DimensionsJson: `{"gift_id":"gift_1001","room_id":"room-1"}`,
	})
	if err != nil {
		t.Fatalf("consume hit event failed: %v", err)
	}
	if consumed.GetStatus() != taskdomain.EventStatusConsumed || consumed.GetMatchedTaskCount() != 1 {
		t.Fatalf("hit event result mismatch: %+v", consumed)
	}

	claimable := listDailyTaskViaGRPC(t, appClient, 10001, taskID)
	if claimable.GetStatus() != taskdomain.StatusCompleted || !claimable.GetClaimable() || claimable.GetProgressValue() != 120 {
		t.Fatalf("task should be completed after matching event: %+v", claimable)
	}

	// App 领取接口必须带当前 task_day 和 command_id；钱包入账只在 activity-service 校验完成态后触发。
	claim, err := appClient.ClaimTaskReward(ctx, &activityv1.ClaimTaskRewardRequest{
		Meta:      taskFlowMeta("task-claim"),
		UserId:    10001,
		TaskId:    taskID,
		TaskType:  taskdomain.TypeDaily,
		TaskDay:   claimable.GetTaskDay(),
		CommandId: "cmd-task-flow-claim",
	})
	if err != nil {
		t.Fatalf("claim task reward failed: %v", err)
	}
	if !claim.GetClaimed() || claim.GetWalletTransactionId() != "wtx-task-flow-1" || claim.GetRewardCoinAmount() != 20 {
		t.Fatalf("claim response mismatch: %+v", claim)
	}
	if wallet.calls != 1 || wallet.last == nil || wallet.last.GetTaskId() != taskID || wallet.last.GetTargetUserId() != 10001 || wallet.last.GetAmount() != 20 {
		t.Fatalf("wallet request mismatch: calls=%d last=%+v", wallet.calls, wallet.last)
	}

	// 同一个 command_id 重放领取接口应复用已发放 claim，不允许重复调用 wallet-service。
	replay, err := appClient.ClaimTaskReward(ctx, &activityv1.ClaimTaskRewardRequest{
		Meta:      taskFlowMeta("task-claim-replay"),
		UserId:    10001,
		TaskId:    taskID,
		TaskType:  taskdomain.TypeDaily,
		TaskDay:   claimable.GetTaskDay(),
		CommandId: "cmd-task-flow-claim",
	})
	if err != nil {
		t.Fatalf("replay claim failed: %v", err)
	}
	if replay.GetClaimId() != claim.GetClaimId() || wallet.calls != 1 {
		t.Fatalf("claim replay should be idempotent: first=%+v replay=%+v wallet_calls=%d", claim, replay, wallet.calls)
	}

	claimed := listDailyTaskViaGRPC(t, appClient, 10001, taskID)
	if claimed.GetStatus() != taskdomain.StatusClaimed || claimed.GetClaimable() {
		t.Fatalf("task should be claimed after reward grant: %+v", claimed)
	}
}

func newTaskFlowGRPCConn(t *testing.T, service *taskservice.Service) *grpcpkg.ClientConn {
	t.Helper()

	listener := bufconn.Listen(1024 * 1024)
	server := grpcpkg.NewServer()
	activityv1.RegisterTaskServiceServer(server, NewTaskServer(service))
	activityv1.RegisterAdminTaskServiceServer(server, NewAdminTaskServer(service))
	go func() {
		_ = server.Serve(listener)
	}()
	t.Cleanup(func() {
		server.Stop()
		_ = listener.Close()
	})

	conn, err := grpcpkg.NewClient("passthrough:///task-flow-bufconn",
		grpcpkg.WithContextDialer(func(context.Context, string) (net.Conn, error) {
			return listener.Dial()
		}),
		grpcpkg.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		t.Fatalf("dial task flow grpc failed: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })
	return conn
}

func listDailyTaskViaGRPC(t *testing.T, client activityv1.TaskServiceClient, userID int64, taskID string) *activityv1.TaskItem {
	t.Helper()

	resp, err := client.ListUserTasks(context.Background(), &activityv1.ListUserTasksRequest{
		Meta:   taskFlowMeta("task-list"),
		UserId: userID,
	})
	if err != nil {
		t.Fatalf("list user tasks failed: %v", err)
	}
	for _, section := range resp.GetSections() {
		for _, item := range section.GetItems() {
			if item.GetTaskId() == taskID {
				return item
			}
		}
	}
	t.Fatalf("task %s not found in response: %+v", taskID, resp)
	return nil
}

func taskFlowMeta(requestID string) *activityv1.RequestMeta {
	return &activityv1.RequestMeta{
		RequestId: requestID,
		Caller:    "task-flow-test",
		AppCode:   "lalu",
		SentAtMs:  time.Date(2026, 5, 9, 10, 0, 0, 0, time.UTC).UnixMilli(),
	}
}

type taskFlowWallet struct {
	calls int
	last  *walletv1.CreditTaskRewardRequest
}

func (w *taskFlowWallet) CreditTaskReward(_ context.Context, req *walletv1.CreditTaskRewardRequest, _ ...grpcpkg.CallOption) (*walletv1.CreditTaskRewardResponse, error) {
	w.calls++
	w.last = req
	return &walletv1.CreditTaskRewardResponse{
		TransactionId: "wtx-task-flow-1",
		Amount:        req.GetAmount(),
		GrantedAtMs:   time.Date(2026, 5, 9, 10, 1, 0, 0, time.UTC).UnixMilli(),
		Balance:       &walletv1.AssetBalance{AssetType: "COIN", AvailableAmount: 1000 + req.GetAmount()},
	}, nil
}
