package grpc

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

	grpcpkg "google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/test/bufconn"
	"google.golang.org/protobuf/proto"
	activityv1 "hyapp.local/api/proto/activity/v1"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	agencydomain "hyapp/services/activity-service/internal/domain/agencyopening"
	agencyservice "hyapp/services/activity-service/internal/service/agencyopening"
	broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
	"hyapp/services/activity-service/internal/testutil/mysqltest"
)

func TestAgencyOpeningGRPCInterfaceFlow(t *testing.T) {
	repository := mysqltest.NewRepository(t)
	now := time.Now().UTC()
	ownerUserID := int64(42001)
	agencySvc := agencyservice.New(repository, nil, fakeAgencyOpeningFlowSource{
		snapshot: agencydomain.AgencySnapshot{
			AgencyID:          8001,
			RegionID:          12,
			OwnerUserID:       ownerUserID,
			Name:              "Window Agency",
			HostCount:         12,
			AgencyCreatedAtMS: now.Add(-time.Hour).UnixMilli(),
			Status:            "active",
		},
	}, &fakeAgencyOpeningFlowRoom{ownerUserID: ownerUserID})
	conn := newAgencyOpeningFlowGRPCConn(t, agencySvc, repository)

	adminClient := activityv1.NewAdminAgencyOpeningServiceClient(conn)
	appClient := activityv1.NewAgencyOpeningServiceClient(conn)
	roomEventClient := activityv1.NewRoomEventConsumerServiceClient(conn)
	ctx := context.Background()

	createdCycle, err := adminClient.CreateAgencyOpeningCycle(ctx, &activityv1.UpsertAgencyOpeningCycleRequest{
		Meta: agencyOpeningFlowMeta("agency-opening-cycle-create"),
		Cycle: &activityv1.AgencyOpeningCycle{
			RegionId:         12,
			Title:            "Agency Opening Interface Flow",
			Status:           "active",
			StartMs:          now.Add(-time.Minute).UnixMilli(),
			EndMs:            now.Add(48 * time.Hour).UnixMilli(),
			MinHostCount:     10,
			MaxAgencyAgeDays: 30,
			Rewards: []*activityv1.AgencyOpeningReward{{
				RankNo:             1,
				ThresholdCoinSpent: 500,
				RewardCoinAmount:   50,
			}},
		},
		OperatorAdminId: 7,
	})
	if err != nil {
		t.Fatalf("create agency opening cycle failed: %v", err)
	}
	if !createdCycle.GetCreated() || createdCycle.GetCycle().GetCycleId() == "" {
		t.Fatalf("create cycle response mismatch: %+v", createdCycle)
	}

	beforeApply, err := appClient.GetAgencyOpeningStatus(ctx, &activityv1.GetAgencyOpeningStatusRequest{
		Meta:   agencyOpeningFlowMeta("agency-opening-status-before-apply"),
		UserId: ownerUserID,
	})
	if err != nil {
		t.Fatalf("get status before apply failed: %v", err)
	}
	if beforeApply.GetJoined() || !beforeApply.GetEligible() {
		t.Fatalf("owner should be eligible before apply: %+v", beforeApply)
	}

	applied, err := appClient.ApplyAgencyOpening(ctx, &activityv1.ApplyAgencyOpeningRequest{
		Meta:      agencyOpeningFlowMeta("agency-opening-apply"),
		UserId:    ownerUserID,
		CommandId: "cmd-agency-opening-flow-apply",
	})
	if err != nil {
		t.Fatalf("apply agency opening failed: %v", err)
	}
	if !applied.GetCreated() || applied.GetApplication().GetStatus() != agencydomain.ApplicationStatusApplied || applied.GetApplication().GetScoreStartMs() != 0 {
		t.Fatalf("apply response mismatch: %+v", applied)
	}

	consumeAgencyOpeningFlowGift(t, ctx, roomEventClient, "gift-before-approval", "room-flow-1", ownerUserID, now.UnixMilli(), 700)

	approved, err := adminClient.ReviewAgencyOpeningApplication(ctx, &activityv1.ReviewAgencyOpeningApplicationRequest{
		Meta:            agencyOpeningFlowMeta("agency-opening-approve"),
		ApplicationId:   applied.GetApplication().GetApplicationId(),
		Action:          "approve",
		OperatorAdminId: 7,
	})
	if err != nil {
		t.Fatalf("approve agency opening failed: %v", err)
	}
	if approved.GetApplication().GetStatus() != agencydomain.ApplicationStatusApproved ||
		approved.GetApplication().GetScoreStartMs() != approved.GetApplication().GetApprovedAtMs() ||
		approved.GetApplication().GetScoreEndMs() <= approved.GetApplication().GetScoreStartMs() {
		t.Fatalf("approve response should open a scoring window: %+v", approved)
	}

	insideWindowMS := approved.GetApplication().GetScoreStartMs() + int64(time.Second.Milliseconds())
	consumeAgencyOpeningFlowGift(t, ctx, roomEventClient, "gift-inside-window", "room-flow-1", ownerUserID, insideWindowMS, 900)

	afterInside, err := appClient.GetAgencyOpeningStatus(ctx, &activityv1.GetAgencyOpeningStatusRequest{
		Meta:   agencyOpeningFlowMeta("agency-opening-status-after-inside"),
		UserId: ownerUserID,
	})
	if err != nil {
		t.Fatalf("get status after inside-window gift failed: %v", err)
	}
	if afterInside.GetApplication().GetScoreCoinAmount() != 900 ||
		afterInside.GetApplication().GetScoreEndMs() != approved.GetApplication().GetScoreEndMs() {
		t.Fatalf("inside-window gift should be counted once: %+v", afterInside.GetApplication())
	}

	consumeAgencyOpeningFlowGift(t, ctx, roomEventClient, "gift-at-window-end", "room-flow-1", ownerUserID, approved.GetApplication().GetScoreEndMs(), 1200)

	afterWindow, err := appClient.GetAgencyOpeningStatus(ctx, &activityv1.GetAgencyOpeningStatusRequest{
		Meta:   agencyOpeningFlowMeta("agency-opening-status-after-window"),
		UserId: ownerUserID,
	})
	if err != nil {
		t.Fatalf("get status after window-end gift failed: %v", err)
	}
	if afterWindow.GetApplication().GetScoreCoinAmount() != 900 {
		t.Fatalf("window-end gift must not be counted: %+v", afterWindow.GetApplication())
	}
}

func newAgencyOpeningFlowGRPCConn(t *testing.T, agencySvc *agencyservice.Service, repository *mysqltest.Repository) *grpcpkg.ClientConn {
	t.Helper()

	listener := bufconn.Listen(1024 * 1024)
	server := grpcpkg.NewServer()
	activityv1.RegisterAgencyOpeningServiceServer(server, NewAgencyOpeningServer(agencySvc))
	activityv1.RegisterAdminAgencyOpeningServiceServer(server, NewAdminAgencyOpeningServer(agencySvc))
	broadcastSvc := broadcastservice.New(broadcastservice.Config{SuperGiftMinValue: 1_000_000_000}, repository, nil, nil)
	activityv1.RegisterRoomEventConsumerServiceServer(server, NewBroadcastServer(broadcastSvc, nil, nil, nil, nil, agencySvc))
	go func() {
		_ = server.Serve(listener)
	}()
	t.Cleanup(func() {
		server.Stop()
		_ = listener.Close()
	})

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

func consumeAgencyOpeningFlowGift(t *testing.T, ctx context.Context, client activityv1.RoomEventConsumerServiceClient, eventID string, roomID string, ownerUserID int64, occurredAtMS int64, giftValue int64) {
	t.Helper()

	body, err := proto.Marshal(&roomeventsv1.RoomGiftSent{
		SenderUserId:    10001,
		TargetUserId:    ownerUserID,
		GiftValue:       giftValue,
		CoinSpent:       giftValue,
		VisibleRegionId: 12,
	})
	if err != nil {
		t.Fatalf("marshal room gift event failed: %v", err)
	}
	if _, err := client.ConsumeRoomEvent(ctx, &activityv1.ConsumeRoomEventRequest{
		Meta: agencyOpeningFlowMeta("consume-" + eventID),
		Envelope: &roomeventsv1.EventEnvelope{
			EventId:      eventID,
			RoomId:       roomID,
			EventType:    "RoomGiftSent",
			OccurredAtMs: occurredAtMS,
			AppCode:      "lalu",
			Body:         body,
		},
	}); err != nil {
		t.Fatalf("consume room event %s failed: %v", eventID, err)
	}
}

func agencyOpeningFlowMeta(requestID string) *activityv1.RequestMeta {
	return &activityv1.RequestMeta{
		RequestId: requestID,
		Caller:    "agency-opening-interface-flow-test",
		AppCode:   "lalu",
		SentAtMs:  time.Now().UTC().UnixMilli(),
	}
}

type fakeAgencyOpeningFlowSource struct {
	snapshot agencydomain.AgencySnapshot
}

func (f fakeAgencyOpeningFlowSource) ResolveOwnerAgency(context.Context, int64) (agencydomain.AgencySnapshot, bool, error) {
	return f.snapshot, true, nil
}

type fakeAgencyOpeningFlowRoom struct {
	ownerUserID int64
}

func (r *fakeAgencyOpeningFlowRoom) AdminGetRoom(context.Context, *roomv1.AdminGetRoomRequest, ...grpcpkg.CallOption) (*roomv1.AdminGetRoomResponse, error) {
	return &roomv1.AdminGetRoomResponse{Room: &roomv1.AdminRoomListItem{OwnerUserId: r.ownerUserID}}, nil
}
