package services

import (
	"context"
	"database/sql"
	"errors"
	"testing"
	"time"

	"quyun/v2/app/commands/testx"
	"quyun/v2/app/errorx"
	tenantdto "quyun/v2/app/http/tenant/dto"
	tenantjoindto "quyun/v2/app/http/tenantjoin/dto"
	"quyun/v2/database"
	"quyun/v2/database/models"
	"quyun/v2/pkg/consts"

	. "github.com/smartystreets/goconvey/convey"
	"github.com/stretchr/testify/suite"

	_ "go.ipao.vip/atom"
	"go.ipao.vip/atom/contracts"
	"go.ipao.vip/gen/types"
	"go.uber.org/dig"
)

type TenantJoinTestSuiteInjectParams struct {
	dig.In

	DB       *sql.DB
	Initials []contracts.Initial `group:"initials"` // nolint:structcheck
}

type TenantJoinTestSuite struct {
	suite.Suite

	TenantJoinTestSuiteInjectParams
}

func Test_TenantJoin(t *testing.T) {
	providers := testx.Default().With(Provide)
	testx.Serve(providers, t, func(p TenantJoinTestSuiteInjectParams) {
		suite.Run(t, &TenantJoinTestSuite{TenantJoinTestSuiteInjectParams: p})
	})
}

func (s *TenantJoinTestSuite) truncateAll(ctx context.Context) {
	So(database.Truncate(ctx, s.DB,
		models.TableNameTenantInvite,
		models.TableNameTenantJoinRequest,
		models.TableNameTenantUser,
	), ShouldBeNil)
}

func (s *TenantJoinTestSuite) seedInvite(ctx context.Context, tenantID, creatorUserID int64, code string, status consts.TenantInviteStatus, maxUses, usedCount int32, expiresAt time.Time) *models.TenantInvite {
	inv := &models.TenantInvite{
		TenantID:  tenantID,
		UserID:    creatorUserID,
		Code:      code,
		Status:    status,
		MaxUses:   maxUses,
		UsedCount: usedCount,
		ExpiresAt: expiresAt,
		Remark:    "seed",
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}
	So(_db.WithContext(ctx).Create(inv).Error, ShouldBeNil)
	return inv
}

func (s *TenantJoinTestSuite) seedJoinRequest(ctx context.Context, tenantID, userID int64, status consts.TenantJoinRequestStatus) *models.TenantJoinRequest {
	req := &models.TenantJoinRequest{
		TenantID:      tenantID,
		UserID:        userID,
		Status:        status,
		Reason:        "seed",
		DecidedAt:     time.Time{},
		DecidedReason: "",
		CreatedAt:     time.Now().UTC(),
		UpdatedAt:     time.Now().UTC(),
	}
	So(_db.WithContext(ctx).Omit("decided_at", "decided_operator_user_id").Create(req).Error, ShouldBeNil)
	return req
}

func (s *TenantJoinTestSuite) Test_AdminCreateInvite() {
	Convey("Tenant.AdminCreateInvite", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)
		adminUserID := int64(10)

		Convey("参数非法应返回参数错误", func() {
			_, err := Tenant.AdminCreateInvite(ctx, 0, adminUserID, &tenantdto.AdminTenantInviteCreateForm{})
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodeInvalidParameter)

			_, err = Tenant.AdminCreateInvite(ctx, tenantID, 0, &tenantdto.AdminTenantInviteCreateForm{})
			So(err, ShouldNotBeNil)
		})

		Convey("code 为空应自动生成并创建成功", func() {
			out, err := Tenant.AdminCreateInvite(ctx, tenantID, adminUserID, &tenantdto.AdminTenantInviteCreateForm{
				Code:    "",
				MaxUses: loToPtr(0),
				Remark:  "test",
			})
			So(err, ShouldBeNil)
			So(out, ShouldNotBeNil)
			So(out.ID, ShouldBeGreaterThan, 0)
			So(out.Code, ShouldNotBeBlank)
			So(out.Status, ShouldEqual, consts.TenantInviteStatusActive)
		})

		Convey("重复 code 应返回重复错误", func() {
			_, err := Tenant.AdminCreateInvite(ctx, tenantID, adminUserID, &tenantdto.AdminTenantInviteCreateForm{
				Code:    "dup_code",
				MaxUses: loToPtr(0),
			})
			So(err, ShouldBeNil)

			_, err = Tenant.AdminCreateInvite(ctx, tenantID, adminUserID, &tenantdto.AdminTenantInviteCreateForm{
				Code:    "dup_code",
				MaxUses: loToPtr(0),
			})
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodeRecordDuplicated)
		})
	})
}

func (s *TenantJoinTestSuite) Test_AdminDisableInvite() {
	Convey("Tenant.AdminDisableInvite", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)
		adminUserID := int64(10)

		Convey("邀请码不存在应返回记录不存在", func() {
			_, err := Tenant.AdminDisableInvite(ctx, tenantID, adminUserID, 999, "x")
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodeRecordNotFound)
		})

		Convey("禁用应成功且幂等", func() {
			inv := s.seedInvite(ctx, tenantID, adminUserID, "c1", consts.TenantInviteStatusActive, 0, 0, time.Time{})

			out, err := Tenant.AdminDisableInvite(ctx, tenantID, adminUserID, inv.ID, "reason")
			So(err, ShouldBeNil)
			So(out.Status, ShouldEqual, consts.TenantInviteStatusDisabled)
			So(out.DisabledOperatorUserID, ShouldEqual, adminUserID)
			So(out.DisabledAt.IsZero(), ShouldBeFalse)

			out2, err := Tenant.AdminDisableInvite(ctx, tenantID, adminUserID, inv.ID, "reason2")
			So(err, ShouldBeNil)
			So(out2.Status, ShouldEqual, consts.TenantInviteStatusDisabled)
		})
	})
}

func (s *TenantJoinTestSuite) Test_AdminInvitePage() {
	Convey("Tenant.AdminInvitePage", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)
		adminUserID := int64(10)

		s.seedInvite(ctx, tenantID, adminUserID, "aaa", consts.TenantInviteStatusActive, 0, 0, time.Time{})
		s.seedInvite(ctx, tenantID, adminUserID, "bbb", consts.TenantInviteStatusDisabled, 0, 0, time.Time{})
		s.seedInvite(ctx, tenantID, adminUserID, "ccc", consts.TenantInviteStatusActive, 0, 0, time.Time{})

		Convey("按 status 过滤应只返回匹配项", func() {
			st := consts.TenantInviteStatusActive
			pager, err := Tenant.AdminInvitePage(ctx, tenantID, &tenantdto.AdminTenantInviteListFilter{
				Status: &st,
			})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 2)
		})

		Convey("按 code 模糊过滤应生效", func() {
			code := "bb"
			pager, err := Tenant.AdminInvitePage(ctx, tenantID, &tenantdto.AdminTenantInviteListFilter{
				Code: &code,
			})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 1)
		})
	})
}

func (s *TenantJoinTestSuite) Test_JoinByInvite() {
	Convey("Tenant.JoinByInvite", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)
		adminUserID := int64(10)
		userID := int64(20)

		Convey("邀请码不存在应返回记录不存在", func() {
			_, err := Tenant.JoinByInvite(ctx, tenantID, userID, "not_exist")
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodeRecordNotFound)
		})

		Convey("成功加入应创建成员并消耗邀请码次数", func() {
			inv := s.seedInvite(ctx, tenantID, adminUserID, "code1", consts.TenantInviteStatusActive, 1, 0, time.Now().UTC().Add(10*time.Minute))

			tu, err := Tenant.JoinByInvite(ctx, tenantID, userID, "code1")
			So(err, ShouldBeNil)
			So(tu, ShouldNotBeNil)
			So(tu.TenantID, ShouldEqual, tenantID)
			So(tu.UserID, ShouldEqual, userID)

			var inv2 models.TenantInvite
			So(_db.WithContext(ctx).Where("id = ?", inv.ID).First(&inv2).Error, ShouldBeNil)
			So(inv2.UsedCount, ShouldEqual, 1)
			So(inv2.Status, ShouldEqual, consts.TenantInviteStatusExpired)

			Convey("重复加入应幂等且不再消耗次数", func() {
				_, err := Tenant.JoinByInvite(ctx, tenantID, userID, "code1")
				So(err, ShouldBeNil)

				var inv3 models.TenantInvite
				So(_db.WithContext(ctx).Where("id = ?", inv.ID).First(&inv3).Error, ShouldBeNil)
				So(inv3.UsedCount, ShouldEqual, 1)
			})
		})
	})
}

func (s *TenantJoinTestSuite) Test_CreateJoinRequest() {
	Convey("Tenant.CreateJoinRequest", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)
		userID := int64(20)

		Convey("已是成员应返回前置条件失败", func() {
			So(_db.WithContext(ctx).Create(&models.TenantUser{
				TenantID: tenantID,
				UserID:   userID,
				Role:     types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleMember}),
				Status:   consts.UserStatusVerified,
			}).Error, ShouldBeNil)

			_, err := Tenant.CreateJoinRequest(ctx, tenantID, userID, &tenantjoindto.JoinRequestCreateForm{Reason: "x"})
			So(err, ShouldNotBeNil)

			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodePreconditionFailed)
		})

		Convey("重复提交应返回同一个 pending 申请（幂等）", func() {
			s.truncateAll(ctx)

			out1, err := Tenant.CreateJoinRequest(ctx, tenantID, userID, &tenantjoindto.JoinRequestCreateForm{Reason: "a"})
			So(err, ShouldBeNil)
			So(out1, ShouldNotBeNil)

			out2, err := Tenant.CreateJoinRequest(ctx, tenantID, userID, &tenantjoindto.JoinRequestCreateForm{Reason: "b"})
			So(err, ShouldBeNil)
			So(out2, ShouldNotBeNil)
			So(out2.ID, ShouldEqual, out1.ID)
			So(out2.Status, ShouldEqual, consts.TenantJoinRequestStatusPending)
		})
	})
}

func (s *TenantJoinTestSuite) Test_AdminJoinRequestPage() {
	Convey("Tenant.AdminJoinRequestPage", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)

		s.seedJoinRequest(ctx, tenantID, 11, consts.TenantJoinRequestStatusPending)
		s.seedJoinRequest(ctx, tenantID, 22, consts.TenantJoinRequestStatusRejected)
		s.seedJoinRequest(ctx, tenantID, 33, consts.TenantJoinRequestStatusPending)

		Convey("按 status 过滤应生效", func() {
			st := consts.TenantJoinRequestStatusPending
			pager, err := Tenant.AdminJoinRequestPage(ctx, tenantID, &tenantdto.AdminTenantJoinRequestListFilter{Status: &st})
			So(err, ShouldBeNil)
			So(pager.Total, ShouldEqual, 2)
		})
	})
}

func (s *TenantJoinTestSuite) Test_AdminApproveJoinRequest() {
	Convey("Tenant.AdminApproveJoinRequest", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)
		adminUserID := int64(10)
		userID := int64(20)

		Convey("申请不存在应返回记录不存在", func() {
			_, err := Tenant.AdminApproveJoinRequest(ctx, tenantID, adminUserID, 999, "x")
			So(err, ShouldNotBeNil)
			var appErr *errorx.AppError
			So(errors.As(err, &appErr), ShouldBeTrue)
			So(appErr.Code, ShouldEqual, errorx.CodeRecordNotFound)
		})

		Convey("通过 pending 申请应成功且幂等", func() {
			req := s.seedJoinRequest(ctx, tenantID, userID, consts.TenantJoinRequestStatusPending)

			out, err := Tenant.AdminApproveJoinRequest(ctx, tenantID, adminUserID, req.ID, "ok")
			So(err, ShouldBeNil)
			So(out.Status, ShouldEqual, consts.TenantJoinRequestStatusApproved)
			So(out.DecidedOperatorUserID, ShouldEqual, adminUserID)
			So(out.DecidedAt.IsZero(), ShouldBeFalse)

			var tu models.TenantUser
			So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&tu).Error, ShouldBeNil)

			out2, err := Tenant.AdminApproveJoinRequest(ctx, tenantID, adminUserID, req.ID, "ok2")
			So(err, ShouldBeNil)
			So(out2.Status, ShouldEqual, consts.TenantJoinRequestStatusApproved)
		})
	})
}

func (s *TenantJoinTestSuite) Test_AdminRejectJoinRequest() {
	Convey("Tenant.AdminRejectJoinRequest", s.T(), func() {
		ctx := s.T().Context()
		s.truncateAll(ctx)

		tenantID := int64(1)
		adminUserID := int64(10)
		userID := int64(20)

		Convey("拒绝 pending 申请应成功且幂等", func() {
			req := s.seedJoinRequest(ctx, tenantID, userID, consts.TenantJoinRequestStatusPending)

			out, err := Tenant.AdminRejectJoinRequest(ctx, tenantID, adminUserID, req.ID, "no")
			So(err, ShouldBeNil)
			So(out.Status, ShouldEqual, consts.TenantJoinRequestStatusRejected)
			So(out.DecidedOperatorUserID, ShouldEqual, adminUserID)

			out2, err := Tenant.AdminRejectJoinRequest(ctx, tenantID, adminUserID, req.ID, "no2")
			So(err, ShouldBeNil)
			So(out2.Status, ShouldEqual, consts.TenantJoinRequestStatusRejected)
		})
	})
}

func loToPtr[T any](v T) *T { return &v }
