package workbench

import (
	"context"
	"encoding/base64"
	"os"
	"testing"

	"aigateway.local/core/internal/platform/config"
	"aigateway.local/core/internal/platform/cryptox"
	"aigateway.local/core/internal/platform/database"
)

func TestMarketplaceLifecycle(t *testing.T) {
	databaseURL := os.Getenv("WORKBENCH_TEST_DATABASE_URL")
	if databaseURL == "" {
		t.Skip("WORKBENCH_TEST_DATABASE_URL is not set")
	}
	ctx := context.Background()
	pool, err := database.Open(ctx, config.Database{URL: databaseURL, MaxConns: 8, MinConns: 0})
	if err != nil {
		t.Fatal(err)
	}
	defer pool.Close()
	actorID := "22222222-2222-4222-8222-222222222222"
	portalUserID := "33333333-3333-4333-8333-333333333333"
	cleanup := func() {
		// pgx v5 Exec uses the extended protocol, which rejects multi-command
		// strings, so each statement must run in its own Exec.
		for _, q := range []string{
			`DELETE FROM gateway.marketplace_installations WHERE portal_user_id='` + portalUserID + `'`,
			`DELETE FROM gateway.digital_employees WHERE code='mkt_employee'`,
			`DELETE FROM gateway.skills WHERE code='mkt_skill'`,
			`DELETE FROM gateway.mcp_servers WHERE code='mkt_mcp'`,
			`DELETE FROM gateway.marketplace_categories WHERE name IN ('mkt-分类','mkt-技能分类')`,
			`DELETE FROM gateway.portal_users WHERE id='` + portalUserID + `'`,
		} {
			if _, err := pool.Exec(ctx, q); err != nil {
				t.Fatalf("cleanup: %v", err)
			}
		}
	}
	cleanup()
	defer cleanup()
	// Inserts must come after the initial cleanup, which would otherwise
	// delete the portal user we just created (it is referenced by the
	// marketplace_installations FK).
	if _, err := pool.Exec(ctx, `INSERT INTO gateway.admin_accounts(id,username,password_hash,role) VALUES($1,'market-test','test','superadmin') ON CONFLICT(id) DO NOTHING`, actorID); err != nil {
		t.Fatal(err)
	}
	if _, err := pool.Exec(ctx, `INSERT INTO gateway.portal_users(id,account,name) VALUES($1,'market-user','市场测试') ON CONFLICT(id) DO NOTHING`, portalUserID); err != nil {
		t.Fatal(err)
	}

	assets := NewService(pool)
	key := base64.StdEncoding.EncodeToString(make([]byte, 32))
	cipher, err := cryptox.NewKeyring(key, 1, "", "mkt-test-headers")
	if err != nil {
		t.Fatal(err)
	}
	mcpSvc := NewMCPServerService(assets, cipher, true)
	toolSvc := NewToolService(assets, cipher, true)
	skillSvc := NewSkillService(assets)
	deSvc := NewDigitalEmployeeService(assets, skillSvc, toolSvc, mcpSvc)
	market := NewMarketplaceService(assets, mcpSvc, skillSvc, deSvc)

	globalCat, err := market.CreateCategory(ctx, Category{Name: "mkt-分类", Description: "全局", ResourceType: ""}, actorID)
	if err != nil {
		t.Fatal(err)
	}
	skillCat, err := market.CreateCategory(ctx, Category{Name: "mkt-技能分类", Description: "技能专用", ResourceType: "skill"}, actorID)
	if err != nil {
		t.Fatal(err)
	}
	// A skill category must reject binding to an MCP server.
	if _, err := market.CreateCategory(ctx, Category{Name: "mkt-冲突分类", Description: "", ResourceType: "mcp_server"}, actorID); err != nil {
		t.Fatalf("mcp_server category should be creatable: %v", err)
	}
	cleanupConflict := func() {
		_, _ = pool.Exec(ctx, `DELETE FROM gateway.marketplace_categories WHERE name='mkt-冲突分类'`)
	}
	cleanupConflict()
	defer cleanupConflict()

	mcp, err := mcpSvc.Save(ctx, "", MCPServerInput{
		Code: "mkt_mcp", Name: "测试 MCP", Transport: "streamable-http",
		EndpointURL: "https://mcp.example.com/mcp", Status: "published",
		CategoryID: &globalCat.ID, Enabled: true, Headers: map[string]string{"Authorization": "Bearer test"},
	}, actorID, true)
	if err != nil {
		t.Fatal(err)
	}
	skill, err := skillSvc.Save(ctx, "", SkillInput{
		Code: "mkt_skill", Name: "测试技能", Content: "你是客服，请回答 {{question}}",
		Variables: []Variable{{Name: "question", Required: true}}, Status: "published",
		CategoryID: &skillCat.ID, Enabled: true, MCPServerIDs: []string{mcp.ID},
	}, actorID, true)
	if err != nil {
		t.Fatal(err)
	}
	rendered, err := skillSvc.Render(skill, map[string]any{"question": "你好"})
	if err != nil || rendered != "你是客服，请回答 你好" {
		t.Fatalf("render=%q err=%v", rendered, err)
	}

	employee, err := deSvc.Save(ctx, "", DigitalEmployeeInput{
		Code: "mkt_employee", Name: "客服机器人", Persona: "你是一位耐心的客服", Model: "deepseek-chat",
		SkillIDs: []string{skill.ID}, Status: "draft", Enabled: true,
	}, actorID, true)
	if err != nil {
		t.Fatal(err)
	}
	if employee.Status != "draft" {
		t.Fatalf("expected draft, got %s", employee.Status)
	}
	published, err := deSvc.Publish(ctx, employee.ID, actorID)
	if err != nil {
		t.Fatalf("publish failed: %v", err)
	}
	if published.Status != "published" {
		t.Fatalf("expected published, got %s", published.Status)
	}

	// Unified catalog merges all three published resource types.
	items, err := market.Catalog(ctx, "", "", "", "", 50)
	if err != nil {
		t.Fatal(err)
	}
	found := map[string]bool{}
	for _, item := range items {
		found[item.Code] = true
	}
	for _, code := range []string{"mkt_mcp", "mkt_skill", "mkt_employee"} {
		if !found[code] {
			t.Fatalf("catalog missing %s (got %#v)", code, found)
		}
	}
	// Category filter narrows to the skill category.
	skillItems, err := market.Catalog(ctx, "skill", skillCat.ID, "", "", 50)
	if err != nil || len(skillItems) != 1 || skillItems[0].Code != "mkt_skill" {
		t.Fatalf("category filter failed: %#v err=%v", skillItems, err)
	}

	detailItem, raw, err := market.Detail(ctx, "digital_employee", "mkt_employee")
	if err != nil || detailItem.Code != "mkt_employee" || len(raw) == 0 {
		t.Fatalf("detail=%#v err=%v", detailItem, err)
	}

	// Install/uninstall is idempotent and gated by published status.
	created, err := market.Install(ctx, "skill", "mkt_skill", portalUserID)
	if err != nil || !created {
		t.Fatalf("install created=%v err=%v", created, err)
	}
	installed, err := market.Installed(ctx, "skill", skill.ID, portalUserID)
	if err != nil || !installed {
		t.Fatalf("installed=%v err=%v", installed, err)
	}
	createdAgain, err := market.Install(ctx, "skill", "mkt_skill", portalUserID)
	if err != nil || createdAgain {
		t.Fatalf("re-install should be a no-op: created=%v err=%v", createdAgain, err)
	}
	installedList, err := market.ListInstalled(ctx, portalUserID)
	if err != nil || len(installedList) != 1 || installedList[0].Code != "mkt_skill" {
		t.Fatalf("installed list=%#v err=%v", installedList, err)
	}
	if err := market.Uninstall(ctx, "skill", "mkt_skill", portalUserID); err != nil {
		t.Fatal(err)
	}
	installed, err = market.Installed(ctx, "skill", skill.ID, portalUserID)
	if err != nil || installed {
		t.Fatalf("expected uninstalled, got installed=%v err=%v", installed, err)
	}

	// Runs mirroring: publishing a digital employee whose skill is bound to an
	// MCP server must still revalidate cleanly (strict bindings).
	if _, err := deSvc.Publish(ctx, employee.ID, actorID); err != nil {
		t.Fatalf("re-publish after install should pass: %v", err)
	}
}
