package admin

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"strconv"
	"testing"
	"time"

	internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
	"github.com/break/junhong_cmp_fiber/internal/model"
	batchAllocationService "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_allocation"
	grantService "github.com/break/junhong_cmp_fiber/internal/service/shop_series_grant"
	"github.com/break/junhong_cmp_fiber/internal/store/postgres"
	"github.com/break/junhong_cmp_fiber/internal/testutil"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/errors"
	"github.com/break/junhong_cmp_fiber/pkg/middleware"
	"github.com/bytedance/sonic"
	"github.com/gofiber/fiber/v2"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围，仅限测试用。
const testIDMask = 0x7fffffff

// TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase 验证批量分配显式选择并固化到全部记录。
func TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase(t *testing.T) {
	testCases := []struct {
		name     string
		field    string
		expected *string
	}{
		{name: "跟随默认", field: "null"},
		{name: "购买即生效", field: `"from_purchase"`, expected: testutil.StringPointer(constants.PackageExpiryBaseFromPurchase)},
		{name: "实名即生效", field: `"from_activation"`, expected: testutil.StringPointer(constants.PackageExpiryBaseFromActivation)},
	}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			tx, fixture := newExpiryBaseHTTPFixture(t)
			app := fixture.newApp(constants.UserTypeSuperAdmin, 0)
			body := `{"shop_id":` + strconv.FormatUint(uint64(fixture.shop.ID), 10) + `,"series_id":` + strconv.FormatUint(uint64(fixture.series.ID), 10) + `,"expiry_base_override":` + testCase.field + `}`
			status, responseBody := expiryBaseHTTPRequest(t, app, http.MethodPost, "/api/admin/shop-package-allocations/batch", body)
			if status != http.StatusOK {
				t.Fatalf("批量分配失败：status=%d body=%s", status, responseBody)
			}
			var allocations []model.ShopPackageAllocation
			if err := tx.Where("shop_id = ?", fixture.shop.ID).Order("package_id").Find(&allocations).Error; err != nil {
				t.Fatalf("查询套餐分配失败：%v", err)
			}
			if len(allocations) != len(fixture.packages) {
				t.Fatalf("期望创建 %d 条分配，实际 %d", len(fixture.packages), len(allocations))
			}
			for _, allocation := range allocations {
				if !nullableStringEqual(allocation.ExpiryBaseOverride, testCase.expected) {
					t.Fatalf("分配 %d 的覆盖值错误：%v", allocation.ID, allocation.ExpiryBaseOverride)
				}
			}
		})
	}
}

// TestBatchAllocatePackagesHTTPRejectsMissingAndInvalidExpiryBase 验证字段缺失与非法枚举统一拒绝且不写入。
func TestBatchAllocatePackagesHTTPRejectsMissingAndInvalidExpiryBase(t *testing.T) {
	for _, bodySuffix := range []string{"", `,"expiry_base_override":"invalid"`} {
		t.Run(bodySuffix, func(t *testing.T) {
			tx, fixture := newExpiryBaseHTTPFixture(t)
			app := fixture.newApp(constants.UserTypeSuperAdmin, 0)
			body := `{"shop_id":` + strconv.FormatUint(uint64(fixture.shop.ID), 10) + `,"series_id":` + strconv.FormatUint(uint64(fixture.series.ID), 10) + bodySuffix + `}`
			status, responseBody := expiryBaseHTTPRequest(t, app, http.MethodPost, "/api/admin/shop-package-allocations/batch", body)
			if status != http.StatusBadRequest {
				t.Fatalf("期望参数错误，实际 status=%d body=%s", status, responseBody)
			}
			var count int64
			if err := tx.Model(&model.ShopPackageAllocation{}).Where("shop_id = ?", fixture.shop.ID).Count(&count).Error; err != nil || count != 0 {
				t.Fatalf("失败请求不应留下套餐分配：count=%d err=%v", count, err)
			}
		})
	}
}

// TestUpdateAllocationExpiryBaseHTTP 验证 PATCH 的恢复默认、幂等、权限和历史快照隔离。
func TestUpdateAllocationExpiryBaseHTTP(t *testing.T) {
	tx, fixture := newExpiryBaseHTTPFixture(t)
	override := constants.PackageExpiryBaseFromPurchase
	allocation := &model.ShopPackageAllocation{ShopID: fixture.shop.ID, PackageID: fixture.packages[0].ID, AllocatorShopID: 101, CostPrice: 1, RetailPrice: 2, ExpiryBaseOverride: &override, Status: constants.StatusEnabled, ShelfStatus: 1}
	if err := tx.Create(allocation).Error; err != nil {
		t.Fatalf("创建套餐分配失败：%v", err)
	}
	usage := completeExpiryBaseTestUsage(fixture.packages[0].ID)
	if err := tx.Create(usage).Error; err != nil {
		t.Fatalf("创建既有购买记录失败：%v", err)
	}
	app := fixture.newApp(constants.UserTypeSuperAdmin, 0)
	path := "/api/admin/shop-package-allocations/" + strconv.FormatUint(uint64(allocation.ID), 10) + "/expiry-base"

	// 首次 PATCH 验证响应体包含 spec 要求的所有生效条件字段
	{
		status, respBody := expiryBaseHTTPRequest(t, app, http.MethodPatch, path, `{"expiry_base_override":"from_activation"}`)
		if status != http.StatusOK {
			t.Fatalf("首次 PATCH 失败：status=%d body=%s", status, respBody)
		}
		var resp struct {
			Data struct {
				DefaultExpiryBase       string  `json:"default_expiry_base"`
				DefaultExpiryBaseName   string  `json:"default_expiry_base_name"`
				ExpiryBaseOverride      *string `json:"expiry_base_override"`
				ExpiryBaseOverrideName  string  `json:"expiry_base_override_name"`
				EffectiveExpiryBase     string  `json:"effective_expiry_base"`
				EffectiveExpiryBaseName string  `json:"effective_expiry_base_name"`
			} `json:"data"`
		}
		if err := sonic.Unmarshal(respBody, &resp); err != nil {
			t.Fatalf("解析 PATCH 响应失败：%v", err)
		}
		d := resp.Data
		if d.DefaultExpiryBase == "" || d.DefaultExpiryBaseName == "" ||
			d.ExpiryBaseOverride == nil || d.ExpiryBaseOverrideName == "" ||
			d.EffectiveExpiryBase == "" || d.EffectiveExpiryBaseName == "" {
			t.Fatalf("PATCH 响应缺少 spec 要求的生效条件字段：%+v", d)
		}
	}
	// 幂等 + 恢复默认
	for _, body := range []string{`{"expiry_base_override":"from_activation"}`, `{"expiry_base_override":null}`} {
		status, responseBody := expiryBaseHTTPRequest(t, app, http.MethodPatch, path, body)
		if status != http.StatusOK {
			t.Fatalf("修改覆盖值失败：status=%d body=%s", status, responseBody)
		}
	}
	var refreshed model.ShopPackageAllocation
	if err := tx.First(&refreshed, allocation.ID).Error; err != nil || refreshed.ExpiryBaseOverride != nil {
		t.Fatalf("显式 null 应恢复跟随默认：allocation=%+v err=%v", refreshed, err)
	}
	var refreshedUsage model.PackageUsage
	if err := tx.First(&refreshedUsage, usage.ID).Error; err != nil {
		t.Fatalf("查询既有购买记录失败：%v", err)
	}
	if refreshedUsage.ExpiryBaseSnapshot != usage.ExpiryBaseSnapshot || refreshedUsage.CalendarTypeSnapshot != usage.CalendarTypeSnapshot || refreshedUsage.DurationDaysSnapshot != usage.DurationDaysSnapshot {
		t.Fatalf("修改分配不应改变既有购买快照：before=%+v after=%+v", usage, refreshedUsage)
	}

	for _, body := range []string{`{}`, `{"expiry_base_override":"invalid"}`} {
		status, _ := expiryBaseHTTPRequest(t, app, http.MethodPatch, path, body)
		if status != http.StatusBadRequest {
			t.Fatalf("字段缺失或非法值应返回 400，body=%s status=%d", body, status)
		}
	}
	forbiddenApp := fixture.newApp(constants.UserTypeAgent, 202)
	status, _ := expiryBaseHTTPRequest(t, forbiddenApp, http.MethodPatch, path, `{"expiry_base_override":null}`)
	if status != http.StatusForbidden {
		t.Fatalf("越权修改应返回 403，实际 %d", status)
	}
	if err := tx.Delete(&refreshed).Error; err != nil {
		t.Fatalf("软删除分配失败：%v", err)
	}
	status, _ = expiryBaseHTTPRequest(t, app, http.MethodPatch, path, `{"expiry_base_override":null}`)
	if status != http.StatusForbidden {
		t.Fatalf("软删除分配应使用统一 403 语义，实际 %d", status)
	}
}

type expiryBaseHTTPFixture struct {
	tx       *gorm.DB
	shop     *model.Shop
	series   *model.PackageSeries
	packages []*model.Package
}

func newExpiryBaseHTTPFixture(t *testing.T) (*gorm.DB, *expiryBaseHTTPFixture) {
	t.Helper()
	tx := testutil.NewPostgresTransaction(t)
	suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
	shop := &model.Shop{ShopName: "UR55测试店铺", ShopCode: "UR55-SHOP-" + suffix, Status: constants.StatusEnabled}
	series := &model.PackageSeries{SeriesCode: "UR55-SERIES-" + suffix, SeriesName: "UR55测试系列", Status: constants.StatusEnabled}
	if err := tx.Create(shop).Error; err != nil {
		t.Fatalf("创建测试店铺失败：%v", err)
	}
	if err := tx.Create(series).Error; err != nil {
		t.Fatalf("创建测试系列失败：%v", err)
	}
	packages := make([]*model.Package, 0, 2)
	for index, expiryBase := range []string{constants.PackageExpiryBaseFromActivation, constants.PackageExpiryBaseFromPurchase} {
		pkg := &model.Package{PackageCode: "UR55-PKG-" + suffix + "-" + strconv.Itoa(index), PackageName: "UR55测试套餐", SeriesID: series.ID, PackageType: constants.PackageTypeFormal, DurationMonths: 1, DurationDays: 30, CalendarType: constants.PackageCalendarTypeByDay, ExpiryBase: expiryBase, Status: constants.StatusEnabled, ShelfStatus: 1, PriceConfigStatus: 2}
		if err := tx.Create(pkg).Error; err != nil {
			t.Fatalf("创建测试套餐失败：%v", err)
		}
		packages = append(packages, pkg)
	}
	seriesAllocation := &model.ShopSeriesAllocation{ShopID: shop.ID, SeriesID: series.ID, Status: constants.StatusEnabled}
	if err := tx.Create(seriesAllocation).Error; err != nil {
		t.Fatalf("创建系列授权失败：%v", err)
	}
	return tx, &expiryBaseHTTPFixture{tx: tx, shop: shop, series: series, packages: packages}
}

func (f *expiryBaseHTTPFixture) newApp(userType int, shopID uint) *fiber.App {
	service := batchAllocationService.New(f.tx, postgres.NewPackageStore(f.tx), postgres.NewShopPackageAllocationStore(f.tx), postgres.NewShopSeriesAllocationStore(f.tx), postgres.NewShopStore(f.tx, nil), nil)
	handler := NewShopPackageBatchAllocationHandler(service)
	app := fiber.New(fiber.Config{JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, ErrorHandler: internalMiddleware.ErrorHandler(zap.NewNop())})
	setContext := func(c *fiber.Ctx) error {
		ctx := middleware.SetUserContext(c.UserContext(), &middleware.UserContextInfo{UserID: 9001, UserType: userType, Username: "UR55测试账号", ShopID: shopID})
		c.SetUserContext(ctx)
		return c.Next()
	}
	app.Post("/api/admin/shop-package-allocations/batch", setContext, handler.BatchAllocate)
	app.Patch("/api/admin/shop-package-allocations/:id/expiry-base", setContext, handler.UpdateExpiryBase)
	return app
}

func expiryBaseHTTPRequest(t *testing.T, app *fiber.App, method, path, body string) (int, []byte) {
	t.Helper()
	request, err := http.NewRequestWithContext(context.Background(), method, path, bytes.NewBufferString(body))
	if err != nil {
		t.Fatalf("创建 HTTP 请求失败：%v", err)
	}
	request.Header.Set("Content-Type", "application/json")
	response, err := app.Test(request, -1)
	if err != nil {
		t.Fatalf("执行 HTTP 请求失败：%v", err)
	}
	defer response.Body.Close()
	responseBody, err := io.ReadAll(response.Body)
	if err != nil {
		t.Fatalf("读取 HTTP 响应失败：%v", err)
	}
	return response.StatusCode, responseBody
}

func completeExpiryBaseTestUsage(packageID uint) *model.PackageUsage {
	unique := uint(time.Now().UnixNano() & testIDMask)
	return &model.PackageUsage{OrderID: unique, OrderNo: "UR55-PATCH-USAGE", PackageID: packageID, UsageType: constants.AssetWalletResourceTypeIotCard, IotCardID: unique, DataLimitMB: 1, Status: constants.PackageUsageStatusPending, Priority: 1, PackageName: "UR55测试套餐", Generation: 1, ExpiryBaseSnapshot: constants.PackageExpiryBaseFromPurchase, CalendarTypeSnapshot: constants.PackageCalendarTypeByDay, DurationDaysSnapshot: 30}
}

func nullableStringEqual(left, right *string) bool {
	return left == nil && right == nil || left != nil && right != nil && *left == *right
}

// TestSeriesGrantCreateHTTPRequiresExplicitExpiryBase 验证系列首次授权（含套餐）固化生效条件到分配记录。
func TestSeriesGrantCreateHTTPRequiresExplicitExpiryBase(t *testing.T) {
	testCases := []struct {
		name     string
		field    string
		expected *string
	}{
		{name: "跟随默认", field: "null"},
		{name: "购买即生效", field: `"from_purchase"`, expected: testutil.StringPointer(constants.PackageExpiryBaseFromPurchase)},
		{name: "实名即生效", field: `"from_activation"`, expected: testutil.StringPointer(constants.PackageExpiryBaseFromActivation)},
	}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			tx, fixture := newExpiryBaseHTTPFixture(t)
			targetShop := &model.Shop{ShopName: "UR55授权目标店铺", ShopCode: "UR55-TARGET-" + strconv.FormatInt(time.Now().UnixNano(), 10), Status: constants.StatusEnabled}
			if err := tx.Create(targetShop).Error; err != nil {
				t.Fatalf("创建目标店铺失败：%v", err)
			}
			app := fixture.newGrantApp(constants.UserTypeSuperAdmin, 0, tx)
			pkg := fixture.packages[0]
			body := `{"shop_id":` + strconv.FormatUint(uint64(targetShop.ID), 10) +
				`,"series_id":` + strconv.FormatUint(uint64(fixture.series.ID), 10) +
				`,"packages":[{"package_id":` + strconv.FormatUint(uint64(pkg.ID), 10) + `,"cost_price":100}]` +
				`,"expiry_base_override":` + testCase.field + `}`
			status, responseBody := expiryBaseHTTPRequest(t, app, http.MethodPost, "/api/admin/shop-series-grants", body)
			if status != http.StatusOK {
				t.Fatalf("系列授权创建失败：status=%d body=%s", status, responseBody)
			}
			var alloc model.ShopPackageAllocation
			if err := tx.Where("shop_id = ? AND package_id = ?", targetShop.ID, pkg.ID).First(&alloc).Error; err != nil {
				t.Fatalf("查询套餐分配失败：%v", err)
			}
			if !nullableStringEqual(alloc.ExpiryBaseOverride, testCase.expected) {
				t.Fatalf("系列授权覆盖值错误：got=%v want=%v", alloc.ExpiryBaseOverride, testCase.expected)
			}
		})
	}
}

// TestSeriesGrantCreateHTTPRejectsMissingAndInvalidExpiryBase 验证字段缺失与非法枚举拒绝系列授权创建。
func TestSeriesGrantCreateHTTPRejectsMissingAndInvalidExpiryBase(t *testing.T) {
	for _, bodySuffix := range []string{"", `,"expiry_base_override":"invalid"`} {
		t.Run(bodySuffix, func(t *testing.T) {
			tx, fixture := newExpiryBaseHTTPFixture(t)
			targetShop := &model.Shop{ShopName: "UR55授权目标店铺", ShopCode: "UR55-TARGET-" + strconv.FormatInt(time.Now().UnixNano(), 10), Status: constants.StatusEnabled}
			if err := tx.Create(targetShop).Error; err != nil {
				t.Fatalf("创建目标店铺失败：%v", err)
			}
			app := fixture.newGrantApp(constants.UserTypeSuperAdmin, 0, tx)
			pkg := fixture.packages[0]
			body := `{"shop_id":` + strconv.FormatUint(uint64(targetShop.ID), 10) +
				`,"series_id":` + strconv.FormatUint(uint64(fixture.series.ID), 10) +
				`,"packages":[{"package_id":` + strconv.FormatUint(uint64(pkg.ID), 10) + `,"cost_price":100}]` +
				bodySuffix + `}`
			status, responseBody := expiryBaseHTTPRequest(t, app, http.MethodPost, "/api/admin/shop-series-grants", body)
			if status != http.StatusBadRequest {
				t.Fatalf("期望 400，实际 status=%d body=%s", status, responseBody)
			}
			var count int64
			if err := tx.Model(&model.ShopPackageAllocation{}).Where("shop_id = ?", targetShop.ID).Count(&count).Error; err != nil || count != 0 {
				t.Fatalf("失败请求不应留下套餐分配：count=%d err=%v", count, err)
			}
		})
	}
}

// TestSeriesGrantManagePackagesHTTPRequiresExplicitExpiryBase 验证后续追加套餐固化生效条件到新分配记录。
func TestSeriesGrantManagePackagesHTTPRequiresExplicitExpiryBase(t *testing.T) {
	tx, fixture := newExpiryBaseHTTPFixture(t)
	targetShop := &model.Shop{ShopName: "UR55追加目标店铺", ShopCode: "UR55-MANAGE-" + strconv.FormatInt(time.Now().UnixNano(), 10), Status: constants.StatusEnabled}
	if err := tx.Create(targetShop).Error; err != nil {
		t.Fatalf("创建目标店铺失败：%v", err)
	}
	// 先创建不含套餐的系列授权
	seriesAlloc := &model.ShopSeriesAllocation{ShopID: targetShop.ID, SeriesID: fixture.series.ID, AllocatorShopID: 0, Status: constants.StatusEnabled}
	if err := tx.Create(seriesAlloc).Error; err != nil {
		t.Fatalf("创建系列授权失败：%v", err)
	}
	app := fixture.newGrantApp(constants.UserTypeSuperAdmin, 0, tx)
	path := "/api/admin/shop-series-grants/" + strconv.FormatUint(uint64(seriesAlloc.ID), 10) + "/packages"
	pkg := fixture.packages[0]

	// 有效追加（from_purchase）
	body := `{"packages":[{"package_id":` + strconv.FormatUint(uint64(pkg.ID), 10) + `,"cost_price":50}],"expiry_base_override":"from_purchase"}`
	status, responseBody := expiryBaseHTTPRequest(t, app, http.MethodPut, path, body)
	if status != http.StatusOK {
		t.Fatalf("追加套餐失败：status=%d body=%s", status, responseBody)
	}
	var alloc model.ShopPackageAllocation
	if err := tx.Where("shop_id = ? AND package_id = ?", targetShop.ID, pkg.ID).First(&alloc).Error; err != nil {
		t.Fatalf("查询新建套餐分配失败：%v", err)
	}
	expected := constants.PackageExpiryBaseFromPurchase
	if !nullableStringEqual(alloc.ExpiryBaseOverride, &expected) {
		t.Fatalf("追加套餐覆盖值错误：%v", alloc.ExpiryBaseOverride)
	}

	// 缺失 expiry_base_override 字段应返回 400
	for _, badBody := range []string{
		`{"packages":[{"package_id":` + strconv.FormatUint(uint64(fixture.packages[1].ID), 10) + `,"cost_price":50}]}`,
		`{"packages":[{"package_id":` + strconv.FormatUint(uint64(fixture.packages[1].ID), 10) + `,"cost_price":50}],"expiry_base_override":"invalid"}`,
	} {
		s, _ := expiryBaseHTTPRequest(t, app, http.MethodPut, path, badBody)
		if s != http.StatusBadRequest {
			t.Fatalf("缺失/非法 expiry_base_override 应返回 400，实际 %d", s)
		}
	}
}

func (f *expiryBaseHTTPFixture) newGrantApp(userType int, shopID uint, tx *gorm.DB) *fiber.App {
	svc := grantService.New(
		tx,
		postgres.NewShopSeriesAllocationStore(tx),
		postgres.NewShopPackageAllocationStore(tx),
		postgres.NewShopPackageAllocationPriceHistoryStore(tx),
		postgres.NewShopStore(tx, nil),
		postgres.NewPackageStore(tx),
		postgres.NewPackageSeriesStore(tx),
		zap.NewNop(),
	)
	handler := NewShopSeriesGrantHandler(svc)
	app := fiber.New(fiber.Config{JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, ErrorHandler: internalMiddleware.ErrorHandler(zap.NewNop())})
	setContext := func(c *fiber.Ctx) error {
		ctx := middleware.SetUserContext(c.UserContext(), &middleware.UserContextInfo{UserID: 9001, UserType: userType, Username: "UR55测试账号", ShopID: shopID})
		c.SetUserContext(ctx)
		return c.Next()
	}
	app.Post("/api/admin/shop-series-grants", setContext, handler.Create)
	app.Put("/api/admin/shop-series-grants/:id/packages", setContext, handler.ManagePackages)
	return app
}

var _ = errors.CodeSuccess
