package agencyopening

import (
	"context"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	domain "hyapp/services/activity-service/internal/domain/agencyopening"
)

func TestEligibleRequiresHostCountGreaterThanConfigured(t *testing.T) {
	cycle := domain.Cycle{RegionID: 12, MinHostCount: 10}
	agency := domain.AgencySnapshot{RegionID: 12, Status: "active", HostCount: 10}
	if ok, reason := eligible(cycle, agency, 1000); ok || reason != "host_count_not_enough" {
		t.Fatalf("host_count equal to threshold must be rejected, got ok=%v reason=%q", ok, reason)
	}
	agency.HostCount = 11
	if ok, reason := eligible(cycle, agency, 1000); !ok || reason != "" {
		t.Fatalf("host_count above threshold must be eligible, got ok=%v reason=%q", ok, reason)
	}
	cycle.RegionID = 0
	agency.RegionID = 99
	if ok, reason := eligible(cycle, agency, 1000); !ok || reason != "" {
		t.Fatalf("global cycle must be eligible across regions, got ok=%v reason=%q", ok, reason)
	}
}

func TestGetStatusRejectsOwnerAlreadyAppliedInAnyCycle(t *testing.T) {
	repo := &agencyOpeningFakeRepo{
		currentCycle: domain.Cycle{CycleID: "current", RegionID: 12, Status: domain.StatusActive, StartMS: 1, EndMS: 10000, MinHostCount: 10},
		hasCurrent:   true,
		application: domain.Application{
			ApplicationID:     "old-application",
			CycleID:           "previous",
			AgencyOwnerUserID: 42,
			Status:            domain.ApplicationStatusApplied,
		},
		hasApplication: true,
	}
	svc := New(repo, nil, agencyOpeningFakeAgencySource{
		snapshot: domain.AgencySnapshot{AgencyID: 7, RegionID: 12, OwnerUserID: 42, Status: "active", HostCount: 11},
		ok:       true,
	}, nil)
	svc.now = func() time.Time { return time.UnixMilli(5000).UTC() }

	status, err := svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 42)
	if err != nil {
		t.Fatalf("GetStatus returned error: %v", err)
	}
	if !status.Joined || status.Eligible || status.IneligibleReason != "already_applied" {
		t.Fatalf("expected already-applied owner to be blocked, got joined=%v eligible=%v reason=%q", status.Joined, status.Eligible, status.IneligibleReason)
	}
	if repo.ownerLookup != 42 {
		t.Fatalf("expected repository lookup by owner_user_id, got %d", repo.ownerLookup)
	}
}

func TestGetStatusRequiresAgencyOwner(t *testing.T) {
	repo := &agencyOpeningFakeRepo{
		currentCycle: domain.Cycle{CycleID: "current", RegionID: 12, Status: domain.StatusActive, StartMS: 1, EndMS: 10000, MinHostCount: 10},
		hasCurrent:   true,
	}
	svc := New(repo, nil, agencyOpeningFakeAgencySource{ok: false}, nil)
	svc.now = func() time.Time { return time.UnixMilli(5000).UTC() }

	status, err := svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 42)
	if err != nil {
		t.Fatalf("GetStatus returned error: %v", err)
	}
	if status.Joined || status.Eligible || status.IneligibleReason != "active_agency_owner_required" {
		t.Fatalf("expected non-agency user to be blocked, got joined=%v eligible=%v reason=%q", status.Joined, status.Eligible, status.IneligibleReason)
	}
}

func TestHandleRoomEventScoresApplicantRoomOwner(t *testing.T) {
	repo := &agencyOpeningFakeRepo{consumeStatus: domain.EventStatusConsumed}
	room := &agencyOpeningFakeRoomClient{ownerUserID: 42}
	svc := New(repo, nil, nil, room)
	svc.now = func() time.Time { return time.UnixMilli(8000).UTC() }
	body, err := proto.Marshal(&roomeventsv1.RoomGiftSent{
		SenderUserId:    9,
		TargetUserId:    88,
		GiftValue:       900,
		CoinSpent:       1200,
		VisibleRegionId: 12,
	})
	if err != nil {
		t.Fatalf("marshal gift: %v", err)
	}
	count, err := svc.HandleRoomEvent(context.Background(), &roomeventsv1.EventEnvelope{
		EventId:      "gift-event-1",
		RoomId:       "room-1",
		EventType:    "RoomGiftSent",
		OccurredAtMs: 7000,
		AppCode:      "lalu",
		Body:         body,
	})
	if err != nil {
		t.Fatalf("HandleRoomEvent returned error: %v", err)
	}
	if count != 1 {
		t.Fatalf("expected one consumed event, got %d", count)
	}
	if room.roomID != "room-1" {
		t.Fatalf("expected room lookup by event room_id, got %q", room.roomID)
	}
	if repo.consumed.RegionID != 12 || repo.consumed.RoomOwnerUserID != 42 || repo.consumed.TargetUserID != 88 || repo.consumed.CoinSpent != 900 {
		t.Fatalf("unexpected consumed event: %+v", repo.consumed)
	}
}

func TestHandleRoomEventTreatsDeletedRoomAsTerminalSkip(t *testing.T) {
	repo := &agencyOpeningFakeRepo{consumeStatus: domain.EventStatusConsumed}
	room := &agencyOpeningFakeRoomClient{err: status.Error(codes.NotFound, "room not found")}
	svc := New(repo, nil, nil, room)
	body, err := proto.Marshal(&roomeventsv1.RoomGiftSent{SenderUserId: 9, TargetUserId: 88, GiftValue: 900, VisibleRegionId: 12})
	if err != nil {
		t.Fatalf("marshal gift: %v", err)
	}

	count, err := svc.HandleRoomEvent(context.Background(), &roomeventsv1.EventEnvelope{
		EventId: "deleted-room-gift", RoomId: "deleted-room", EventType: "RoomGiftSent", AppCode: "lalu", Body: body,
	})
	if err != nil || count != 0 {
		t.Fatalf("deleted room must be terminal skipped, count=%d err=%v", count, err)
	}
	if repo.consumed.EventID != "" {
		t.Fatalf("deleted room must not create agency turnover: %+v", repo.consumed)
	}
}

func TestHandleRoomEventKeepsTransientRoomFailureRetryable(t *testing.T) {
	repo := &agencyOpeningFakeRepo{consumeStatus: domain.EventStatusConsumed}
	room := &agencyOpeningFakeRoomClient{err: status.Error(codes.Unavailable, "room service unavailable")}
	svc := New(repo, nil, nil, room)
	body, err := proto.Marshal(&roomeventsv1.RoomGiftSent{SenderUserId: 9, TargetUserId: 88, GiftValue: 900, VisibleRegionId: 12})
	if err != nil {
		t.Fatalf("marshal gift: %v", err)
	}

	count, err := svc.HandleRoomEvent(context.Background(), &roomeventsv1.EventEnvelope{
		EventId: "transient-room-gift", RoomId: "room-1", EventType: "RoomGiftSent", AppCode: "lalu", Body: body,
	})
	if err == nil || status.Code(err) != codes.Unavailable || count != 0 {
		t.Fatalf("transient room failure must remain retryable, count=%d err=%v", count, err)
	}
	if repo.consumed.EventID != "" {
		t.Fatalf("transient room failure must not create partial projection: %+v", repo.consumed)
	}
}

type agencyOpeningFakeRepo struct {
	currentCycle   domain.Cycle
	hasCurrent     bool
	application    domain.Application
	hasApplication bool
	ownerLookup    int64
	consumeStatus  string
	consumed       domain.GiftEvent
}

func (r *agencyOpeningFakeRepo) ListAgencyOpeningCycles(context.Context, domain.ListQuery) ([]domain.Cycle, int64, error) {
	return nil, 0, nil
}
func (r *agencyOpeningFakeRepo) GetAgencyOpeningCycle(context.Context, string) (domain.Cycle, error) {
	return domain.Cycle{}, nil
}
func (r *agencyOpeningFakeRepo) UpsertAgencyOpeningCycle(context.Context, domain.CycleCommand, int64) (domain.Cycle, bool, error) {
	return domain.Cycle{}, false, nil
}
func (r *agencyOpeningFakeRepo) SetAgencyOpeningCycleStatus(context.Context, string, string, int64, int64) (domain.Cycle, error) {
	return domain.Cycle{}, nil
}
func (r *agencyOpeningFakeRepo) FindCurrentAgencyOpeningCycle(context.Context, int64, int64) (domain.Cycle, bool, error) {
	return r.currentCycle, r.hasCurrent, nil
}
func (r *agencyOpeningFakeRepo) GetAgencyOpeningApplicationByOwner(_ context.Context, ownerUserID int64) (domain.Application, bool, error) {
	r.ownerLookup = ownerUserID
	return r.application, r.hasApplication, nil
}
func (r *agencyOpeningFakeRepo) CreateAgencyOpeningApplication(context.Context, domain.Cycle, domain.AgencySnapshot, int64) (domain.Application, bool, error) {
	return domain.Application{}, false, nil
}
func (r *agencyOpeningFakeRepo) ApproveAgencyOpeningApplication(context.Context, string, int64, int64) (domain.Application, error) {
	return domain.Application{}, nil
}
func (r *agencyOpeningFakeRepo) ListAgencyOpeningApplications(context.Context, domain.ApplicationQuery) ([]domain.Application, int64, error) {
	return nil, 0, nil
}
func (r *agencyOpeningFakeRepo) ConsumeAgencyOpeningGiftEvent(_ context.Context, event domain.GiftEvent, _ int64) (domain.EventResult, error) {
	r.consumed = event
	return domain.EventResult{EventID: event.EventID, Status: r.consumeStatus}, nil
}
func (r *agencyOpeningFakeRepo) ClaimDueAgencyOpeningCycles(context.Context, string, int64, time.Duration, int32) ([]domain.Cycle, error) {
	return nil, nil
}
func (r *agencyOpeningFakeRepo) PrepareAgencyOpeningSettlements(context.Context, domain.Cycle, int64) ([]domain.Application, error) {
	return nil, nil
}
func (r *agencyOpeningFakeRepo) MarkAgencyOpeningApplicationGranted(context.Context, string, string, int64) (domain.Application, error) {
	return domain.Application{}, nil
}
func (r *agencyOpeningFakeRepo) MarkAgencyOpeningApplicationFailed(context.Context, string, string, int64) error {
	return nil
}
func (r *agencyOpeningFakeRepo) FinishAgencyOpeningCycleIfComplete(context.Context, string, int64) (bool, error) {
	return false, nil
}

type agencyOpeningFakeAgencySource struct {
	snapshot domain.AgencySnapshot
	ok       bool
}

func (s agencyOpeningFakeAgencySource) ResolveOwnerAgency(context.Context, int64) (domain.AgencySnapshot, bool, error) {
	return s.snapshot, s.ok, nil
}

type agencyOpeningFakeRoomClient struct {
	ownerUserID int64
	roomID      string
	err         error
}

func (c *agencyOpeningFakeRoomClient) AdminGetRoom(_ context.Context, req *roomv1.AdminGetRoomRequest, _ ...grpc.CallOption) (*roomv1.AdminGetRoomResponse, error) {
	c.roomID = req.GetRoomId()
	if c.err != nil {
		return nil, c.err
	}
	return &roomv1.AdminGetRoomResponse{
		Room: &roomv1.AdminRoomListItem{OwnerUserId: c.ownerUserID},
	}, nil
}

type agencyOpeningFakeWalletClient struct{}

func (agencyOpeningFakeWalletClient) CreditAgencyOpeningReward(context.Context, *walletv1.CreditAgencyOpeningRewardRequest, ...grpc.CallOption) (*walletv1.CreditAgencyOpeningRewardResponse, error) {
	return &walletv1.CreditAgencyOpeningRewardResponse{}, nil
}
