package orgsso

import (
	"context"
	"errors"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/shopspring/decimal"
	"github.com/smartystreets/goconvey/convey"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	orgdao "github.com/tidbcloud/account/internel/dao/org"
	"github.com/tidbcloud/account/thirdparty/billing2"
	tinfoerr "github.com/tidbcloud/tiinf/errs"
	"gorm.io/gorm"
)

// fakeBillingHandler implements billing2.BillingHandler for tests.
// Only GetTenantPlanInfo is exercised here; other methods are inherited
// from the embedded interface and return zero / nil values.
type fakeBillingHandler struct {
	billing2.BillingHandler
	planInfo *billing2.TenantPlanInfo
	err      error
}

func (f *fakeBillingHandler) GetTenantPlanInfo(_ context.Context, _ uint64) (*billing2.TenantPlanInfo, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.planInfo, nil
}

func isBadRequest(err error) bool {
	if err == nil {
		return false
	}
	if te, ok := tinfoerr.AsTiError(err); ok {
		return te.HTTPCode() == 400
	}
	return false
}

// TestOrgSsoSvc_OpenOrgSso_EffectivePlanGate verifies the security fix:
// SSO enablement must be blocked unless the org's effective billing plan
// is "on_demand" (i.e. a paid organization). Previously the handler had
// no plan / payment gate at all, allowing SSO enablement on free orgs.
func TestOrgSsoSvc_OpenOrgSso_EffectivePlanGate(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	const orgID uint64 = 1372813089209337295

	activePersonalOrg := &entity.Tenant{
		OrgType: entity.OrgTypePersonal,
		Status:  entity.TenantStatusActive,
	}
	activePersonalOrg.ID = entity.ID(orgID)

	makeSvc := func() *OrgSsoSvc {
		mockedHub := dao.NewDaoMockHub(ctrl).NewHub()
		mockedHub.Org.(*orgdao.MockOrgDaoIface).EXPECT().
			GetOrgByOrgID(gomock.Any(), gomock.Any(), orgID).
			Return(activePersonalOrg, nil).
			AnyTimes()
		return &OrgSsoSvc{db: &gorm.DB{}, hub: mockedHub}
	}

	req := &central.OpenOrgSsoReq{
		OrgId:       orgID,
		CompanyName: "test-org-sso",
	}

	convey.Convey("OpenOrgSso blocks SSO when effective plan is not on_demand", t, func() {
		plansToBlock := []string{
			string(entity.TenantPlanNewUser),
			string(entity.TenantPlanFreeTrial),
			string(entity.TenantPlanPOC),
			"closing",
			"",
		}
		original := billing2.GetHandler()
		defer billing2.SetHandler(original)
		for _, plan := range plansToBlock {
			billing2.SetHandler(&fakeBillingHandler{
				planInfo: &billing2.TenantPlanInfo{
					EffectivePlan:     plan,
					EffectiveDiscount: decimal.Zero,
					POCCredits:        decimal.Zero,
				},
			})
			svc := makeSvc()
			err := svc.OpenOrgSso(context.Background(), req)
			convey.So(err, convey.ShouldNotBeNil)
			convey.So(isBadRequest(err), convey.ShouldBeTrue)
		}
	})

	convey.Convey("OpenOrgSso allows on_demand orgs past the plan gate", t, func() {
		original := billing2.GetHandler()
		defer billing2.SetHandler(original)
		billing2.SetHandler(&fakeBillingHandler{
			planInfo: &billing2.TenantPlanInfo{EffectivePlan: string(entity.TenantPlanOnDemand)},
		})

		svc := makeSvc()
		err := svc.OpenOrgSso(context.Background(), req)
		// We expect the call to fail *after* the plan gate — at user-context
		// lookup (no auth in test) — so a non-bad-request error is the signal
		// that the gate passed.
		convey.So(err, convey.ShouldNotBeNil)
		convey.So(isBadRequest(err), convey.ShouldBeFalse)
	})

	convey.Convey("OpenOrgSso returns billing error when GetTenantPlanInfo fails", t, func() {
		original := billing2.GetHandler()
		defer billing2.SetHandler(original)
		billing2.SetHandler(&fakeBillingHandler{err: errors.New("billing down")})

		svc := makeSvc()
		err := svc.OpenOrgSso(context.Background(), req)
		convey.So(err, convey.ShouldNotBeNil)
		// Should NOT be a bad-request "plan not on_demand" error — should
		// bubble up the underlying billing error.
		convey.So(isBadRequest(err), convey.ShouldBeFalse)
	})
}

// TestOrgSsoSvc_GetOrgActiveAuthMethods_AccessControl exercises the
// /api/v1/auth-method handler's public surface (PM-confirmed expected
// behavior: anonymous lookup by org_id or company_name is part of the
// enterprise SSO connection flow). Coverage focuses on input validation
// and the empty / unknown-org paths rather than auth, since the endpoint
// is intentionally on the RBAC pathWhitelist.
func TestOrgSsoSvc_GetOrgActiveAuthMethods_AccessControl(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	const orgID uint64 = 1372813089209337295

	activeOrg := &entity.Tenant{
		OrgType:     entity.OrgTypePersonal,
		Status:      entity.TenantStatusActive,
		CompanyName: "mk-jp-tidb-gcp",
	}
	activeOrg.ID = entity.ID(orgID)

	makeSvc := func() *OrgSsoSvc {
		h := dao.NewDaoMockHub(ctrl).NewHub()
		h.Org.(*orgdao.MockOrgDaoIface).EXPECT().
			GetOrgByOrgID(gomock.Any(), gomock.Any(), orgID).
			Return(activeOrg, nil).
			AnyTimes()
		return &OrgSsoSvc{db: &gorm.DB{}, hub: h}
	}

	convey.Convey("GetOrgActiveAuthMethods rejects empty requests as BadRequest", t, func() {
		svc := makeSvc()
		_, err := svc.GetOrgActiveAuthMethods(context.Background(), &central.ListOrgActiveAuthMethodsReq{})
		convey.So(err, convey.ShouldNotBeNil)
		convey.So(isBadRequest(err), convey.ShouldBeTrue)
	})
}
