package routes

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"testing"
	"time"

	"github.com/bytedance/sonic"
	"github.com/gofiber/fiber/v2"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/break/junhong_cmp_fiber/internal/bootstrap"
	"github.com/break/junhong_cmp_fiber/internal/handler/admin"
	internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
	"github.com/break/junhong_cmp_fiber/internal/model"
	assetQuery "github.com/break/junhong_cmp_fiber/internal/query/asset"
	assetService "github.com/break/junhong_cmp_fiber/internal/service/asset"
	"github.com/break/junhong_cmp_fiber/internal/store/postgres"
	"github.com/break/junhong_cmp_fiber/pkg/auth"
	"github.com/break/junhong_cmp_fiber/pkg/config"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/database"
	"github.com/break/junhong_cmp_fiber/pkg/errors"
	pkgMiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
)

// TestAssetResolveHTTPIntegratesAuthenticationPermissionsAndExchangeTrace 验证真实认证、权限、资产解析和双向换货投影。
func TestAssetResolveHTTPIntegratesAuthenticationPermissionsAndExchangeTrace(t *testing.T) {
	env := newAssetTraceHTTPEnv(t)
	visibleShop := env.createShop(t, "UR86 可见店铺", nil, 1)
	hiddenShop := env.createShop(t, "UR86 不可见店铺", nil, 1)
	oldCard := env.createCard(t, 1, &hiddenShop.ID)
	middleCard := env.createCard(t, 2, &visibleShop.ID)
	nextCard := env.createCard(t, 3, &visibleShop.ID)
	env.createOrder(t, "UR86-HTTP-PREV", oldCard, middleCard, "HTTP 历史前代快照", "HTTP 中间快照", time.Now().Add(-time.Minute))
	env.createOrder(t, "UR86-HTTP-NEXT", middleCard, nextCard, "HTTP 中间快照", "HTTP 历史后代快照", time.Now())
	token := env.newAgentToken(t, visibleShop.ID)

	status, body := env.request(t, "/api/admin/assets/resolve/"+middleCard.ICCID, token)
	if status != http.StatusOK {
		t.Fatalf("查询中间资产失败，状态 %d：%s", status, body)
	}
	var success struct {
		Code int `json:"code"`
		Data struct {
			AssetID       uint `json:"asset_id"`
			ExchangeTrace struct {
				PreviousAsset *struct {
					AssetID    *uint  `json:"asset_id"`
					Identifier string `json:"identifier"`
					ExchangeNo string `json:"exchange_no"`
					CanView    bool   `json:"can_view"`
				} `json:"previous_asset"`
				NextAsset *struct {
					AssetID    *uint  `json:"asset_id"`
					Identifier string `json:"identifier"`
					ExchangeNo string `json:"exchange_no"`
					CanView    bool   `json:"can_view"`
				} `json:"next_asset"`
			} `json:"exchange_trace"`
		} `json:"data"`
	}
	if err := sonic.Unmarshal(body, &success); err != nil {
		t.Fatalf("解析资产详情响应失败：%v", err)
	}
	if success.Code != errors.CodeSuccess || success.Data.AssetID != middleCard.ID || success.Data.ExchangeTrace.PreviousAsset == nil || success.Data.ExchangeTrace.NextAsset == nil {
		t.Fatalf("双向换货响应不完整：%s", body)
	}
	previous := success.Data.ExchangeTrace.PreviousAsset
	if previous.CanView || previous.AssetID != nil || previous.Identifier != "HTTP 历史前代快照" || previous.ExchangeNo != "UR86-HTTP-PREV" {
		t.Fatalf("不可见前代未按契约隐藏 ID：%s", body)
	}
	next := success.Data.ExchangeTrace.NextAsset
	if !next.CanView || next.AssetID == nil || *next.AssetID != nextCard.ID || next.Identifier != "HTTP 历史后代快照" || next.ExchangeNo != "UR86-HTTP-NEXT" {
		t.Fatalf("可见后代未按契约返回：%s", body)
	}

	noTraceCard := env.createCard(t, 4, &visibleShop.ID)
	status, body = env.request(t, "/api/admin/assets/resolve/"+noTraceCard.ICCID, token)
	assertHTTPTraceDirections(t, status, body, false, false)

	previousOnlyOld := env.createCard(t, 5, &visibleShop.ID)
	previousOnlyNew := env.createCard(t, 6, &visibleShop.ID)
	env.createOrder(t, "UR86-HTTP-PREV-ONLY", previousOnlyOld, previousOnlyNew, "仅前代旧快照", "仅前代新快照", time.Now())
	status, body = env.request(t, "/api/admin/assets/resolve/"+previousOnlyNew.ICCID, token)
	assertHTTPTraceDirections(t, status, body, true, false)

	nextOnlyOld := env.createCard(t, 7, &visibleShop.ID)
	nextOnlyNew := env.createCard(t, 8, &visibleShop.ID)
	env.createOrder(t, "UR86-HTTP-NEXT-ONLY", nextOnlyOld, nextOnlyNew, "仅后代旧快照", "仅后代新快照", time.Now())
	status, body = env.request(t, "/api/admin/assets/resolve/"+nextOnlyOld.ICCID, token)
	assertHTTPTraceDirections(t, status, body, false, true)

	status, body = env.request(t, "/api/admin/assets/resolve/"+oldCard.ICCID, token)
	if status != http.StatusNotFound {
		t.Fatalf("当前资产无权限时应维持防枚举响应，状态 %d：%s", status, body)
	}
	var denied struct {
		Code int `json:"code"`
		Data any `json:"data"`
	}
	if err := sonic.Unmarshal(body, &denied); err != nil || denied.Code != errors.CodeNotFound || denied.Data != nil {
		t.Fatalf("当前资产无权限响应不符合原契约：%s", body)
	}
}

func assertHTTPTraceDirections(t *testing.T, status int, body []byte, hasPrevious, hasNext bool) {
	t.Helper()
	if status != http.StatusOK {
		t.Fatalf("资产详情状态错误，实际 %d：%s", status, body)
	}
	var response struct {
		Data struct {
			ExchangeTrace struct {
				PreviousAsset any `json:"previous_asset"`
				NextAsset     any `json:"next_asset"`
			} `json:"exchange_trace"`
		} `json:"data"`
	}
	if err := sonic.Unmarshal(body, &response); err != nil {
		t.Fatalf("解析换货方向响应失败：%v", err)
	}
	if (response.Data.ExchangeTrace.PreviousAsset != nil) != hasPrevious || (response.Data.ExchangeTrace.NextAsset != nil) != hasNext {
		t.Fatalf("换货方向空值语义错误：previous=%v next=%v body=%s", hasPrevious, hasNext, body)
	}
}

type assetTraceHTTPEnv struct {
	app          *fiber.App
	db           *gorm.DB
	redis        *redis.Client
	tokenManager *auth.TokenManager
}

func newAssetTraceHTTPEnv(t *testing.T) *assetTraceHTTPEnv {
	t.Helper()
	if os.Getenv("JUNHONG_DATABASE_HOST") == "" || os.Getenv("JUNHONG_REDIS_ADDRESS") == "" {
		t.Skip("未加载 .env.local，跳过依赖真实 PostgreSQL 和 Redis 的资产换货链 HTTP 集成测试")
	}
	cfg, err := config.Load()
	if err != nil {
		t.Fatalf("加载测试配置失败：%v", err)
	}
	log := zap.NewNop()
	db, err := database.InitPostgreSQL(&cfg.Database, log)
	if err != nil {
		t.Fatalf("连接 PostgreSQL 失败：%v", err)
	}
	tx := db.Begin()
	if tx.Error != nil {
		t.Fatalf("开启资产换货链测试事务失败：%v", tx.Error)
	}
	redisClient, err := database.NewRedisClient(database.RedisConfig{
		Address: cfg.Redis.Address + ":" + strconv.Itoa(cfg.Redis.Port), Password: cfg.Redis.Password, DB: cfg.Redis.DB,
		PoolSize: cfg.Redis.PoolSize, MinIdleConns: cfg.Redis.MinIdleConns,
		DialTimeout: cfg.Redis.DialTimeout, ReadTimeout: cfg.Redis.ReadTimeout, WriteTimeout: cfg.Redis.WriteTimeout,
	}, log)
	if err != nil {
		t.Fatalf("连接 Redis 失败：%v", err)
	}
	t.Cleanup(func() {
		_ = tx.Rollback().Error
		_ = redisClient.Close()
		if sqlDB, dbErr := db.DB(); dbErr == nil {
			_ = sqlDB.Close()
		}
	})

	shopStore := postgres.NewShopStore(tx, redisClient)
	deviceStore := postgres.NewDeviceStore(tx, redisClient)
	cardStore := postgres.NewIotCardStore(tx, redisClient)
	assetSvc := assetService.New(
		tx, deviceStore, cardStore,
		postgres.NewPackageUsageStore(tx, redisClient), postgres.NewPackageStore(tx), postgres.NewPackageSeriesStore(tx),
		postgres.NewDeviceSimBindingStore(tx, redisClient), shopStore, redisClient, nil, nil,
		postgres.NewAssetIdentifierStore(tx), postgres.NewOrderStore(tx, redisClient), postgres.NewOrderItemStore(tx, redisClient),
		postgres.NewExchangeOrderStore(tx), nil,
	)
	handler := admin.NewAssetHandler(assetSvc, nil, nil, nil, nil, nil, assetQuery.NewExchangeTraceQuery(tx, log))
	tokenManager := auth.NewTokenManager(redisClient, cfg.JWT.AccessTokenTTL, cfg.JWT.RefreshTokenTTL)
	authMiddleware := pkgMiddleware.Auth(pkgMiddleware.AuthConfig{
		TokenValidator: func(token string) (*pkgMiddleware.UserContextInfo, error) {
			info, validateErr := tokenManager.ValidateAccessToken(context.Background(), token)
			if validateErr != nil {
				return nil, errors.New(errors.CodeInvalidToken, "认证令牌无效或已过期")
			}
			return &pkgMiddleware.UserContextInfo{UserID: info.UserID, UserType: info.UserType, Username: info.Username, ShopID: info.ShopID}, nil
		},
		ShopStore: shopStore,
	})
	app := fiber.New(fiber.Config{JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, ErrorHandler: internalMiddleware.ErrorHandler(log)})
	RegisterAdminRoutes(app.Group("/api/admin"), &bootstrap.Handlers{Asset: handler}, &bootstrap.Middlewares{AdminAuth: authMiddleware}, nil, "/api/admin")
	return &assetTraceHTTPEnv{app: app, db: tx, redis: redisClient, tokenManager: tokenManager}
}

func (e *assetTraceHTTPEnv) createShop(t *testing.T, name string, parentID *uint, level int) *model.Shop {
	t.Helper()
	shop := &model.Shop{ShopName: name, ShopCode: fmt.Sprintf("UR86-%d", time.Now().UnixNano()), ParentID: parentID, Level: level, Status: constants.ShopStatusEnabled}
	if err := e.db.Create(shop).Error; err != nil {
		t.Fatalf("创建资产换货链测试店铺失败：%v", err)
	}
	return shop
}

func (e *assetTraceHTTPEnv) createCard(t *testing.T, suffix int, shopID *uint) *model.IotCard {
	t.Helper()
	iccid := fmt.Sprintf("8986333333333333%04d", suffix)
	card := &model.IotCard{ICCID: iccid, ICCID19: iccid[:19], VirtualNo: fmt.Sprintf("UR86-HTTP-CARD-%04d", suffix), ShopID: shopID, Status: constants.IotCardStatusInStock, AssetStatus: constants.AssetStatusInStock}
	if err := e.db.Create(card).Error; err != nil {
		t.Fatalf("创建资产换货链 HTTP 测试卡失败：%v", err)
	}
	return card
}

func (e *assetTraceHTTPEnv) createOrder(t *testing.T, exchangeNo string, oldCard, newCard *model.IotCard, oldSnapshot, newSnapshot string, completedAt time.Time) {
	t.Helper()
	newID := newCard.ID
	order := &model.ExchangeOrder{
		ExchangeNo: exchangeNo, FlowType: constants.ExchangeFlowTypeDirect,
		OldAssetType: constants.ExchangeAssetTypeIotCard, OldAssetID: oldCard.ID, OldAssetIdentifier: oldSnapshot,
		NewAssetType: constants.ExchangeAssetTypeIotCard, NewAssetID: &newID, NewAssetIdentifier: newSnapshot,
		ExchangeReason: "UR86 HTTP 测试", Status: constants.ExchangeStatusCompleted, CompletedAt: &completedAt,
	}
	if err := e.db.Create(order).Error; err != nil {
		t.Fatalf("创建资产换货链 HTTP 测试单失败：%v", err)
	}
}

func (e *assetTraceHTTPEnv) newAgentToken(t *testing.T, shopID uint) string {
	t.Helper()
	info := &auth.TokenInfo{UserID: uint(time.Now().UnixNano() % 1_000_000_000), UserType: constants.UserTypeAgent, ShopID: shopID, Username: "UR86测试代理"}
	accessToken, refreshToken, err := e.tokenManager.GenerateTokenPair(context.Background(), info)
	if err != nil {
		t.Fatalf("创建资产换货链测试令牌失败：%v", err)
	}
	t.Cleanup(func() {
		_ = e.tokenManager.RevokeToken(context.Background(), accessToken)
		_ = e.tokenManager.RevokeToken(context.Background(), refreshToken)
		_ = e.redis.Del(context.Background(), constants.RedisUserTokensKey(info.UserID)).Err()
	})
	return accessToken
}

func (e *assetTraceHTTPEnv) request(t *testing.T, path, token string) (int, []byte) {
	t.Helper()
	request, err := http.NewRequest(http.MethodGet, path, nil)
	if err != nil {
		t.Fatalf("创建资产换货链 HTTP 请求失败：%v", err)
	}
	request.Header.Set(fiber.HeaderAuthorization, "Bearer "+token)
	response, err := e.app.Test(request, -1)
	if err != nil {
		t.Fatalf("执行资产换货链 HTTP 请求失败：%v", err)
	}
	defer response.Body.Close()
	body, err := io.ReadAll(response.Body)
	if err != nil {
		t.Fatalf("读取资产换货链 HTTP 响应失败：%v", err)
	}
	return response.StatusCode, body
}
