//go:build integration

package controlplane

import (
	"context"
	"crypto/tls"
	"database/sql"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/go-sql-driver/mysql"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
	"github.com/nexuscrm/db"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestContractLifecycleAutoNumberAndStatusMetadataParity(t *testing.T) {
	ctx := context.Background()
	conn, sm := setupCRMRuntimeForContractLifecycle(t, ctx)
	defer conn.Close()

	admin := systemAdminSession()
	accountID := createAccountForOrderActivation(t, ctx, sm, admin)
	contractID, contractNumber := createDraftContractForLifecycle(t, ctx, sm, admin, accountID)

	if contractNumber == "" {
		t.Fatal("inserted contract returned empty ContractNumber")
	}

	if err := sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"Status": "Activated",
	}, admin); err != nil {
		t.Fatalf("activate contract: %v", err)
	}

	contract, err := sm.Persistence().GetRaw(ctx, "Contract", contractID)
	if err != nil {
		t.Fatalf("get activated contract: %v", err)
	}
	if got := contract["ContractNumber"]; got != contractNumber {
		t.Fatalf("Contract.ContractNumber = %#v, want %q", got, contractNumber)
	}
	if got := contract["Status"]; got != "Activated" {
		t.Fatalf("Contract.Status = %#v, want Activated", got)
	}
	if got := contract["StatusCode"]; got != "Activated" {
		t.Fatalf("Contract.StatusCode = %#v, want Activated", got)
	}
	if got := contract["ActivatedById"]; got != admin.ID {
		t.Fatalf("Contract.ActivatedById = %#v, want %s", got, admin.ID)
	}
	if got := contract["ActivatedDate"]; got == nil || got == "" {
		t.Fatalf("Contract.ActivatedDate = %#v, want non-empty stamp", got)
	}

	err = sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"Status": "Draft",
	}, admin)
	if err == nil || !strings.Contains(strings.ToLower(err.Error()), "status") {
		t.Fatalf("Activated -> Draft error = %v, want status transition rejection", err)
	}
}

func TestContractLifecycleRejectsExplicitContractNumberWrite(t *testing.T) {
	ctx := context.Background()
	conn, sm := setupCRMRuntimeForContractLifecycle(t, ctx)
	defer conn.Close()

	admin := systemAdminSession()
	accountID := createAccountForOrderActivation(t, ctx, sm, admin)

	_, err := sm.Persistence().Insert(ctx, "Contract", models.SObject{
		"AccountId":      accountID,
		"Status":         "Draft",
		"StartDate":      "2026-01-01",
		"EndDate":        "2026-12-31",
		"ContractTerm":   12,
		"ContractNumber": "MANUAL-0001",
	}, admin)
	if err == nil || !strings.Contains(err.Error(), "ContractNumber") {
		t.Fatalf("insert with explicit ContractNumber error = %v, want INVALID_FIELD_FOR_INSERT_UPDATE-like rejection", err)
	}

	contractID, _ := createDraftContractForLifecycle(t, ctx, sm, admin, accountID)
	err = sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"ContractNumber": "MANUAL-0002",
	}, admin)
	if err == nil || !strings.Contains(err.Error(), "ContractNumber") {
		t.Fatalf("update with explicit ContractNumber error = %v, want INVALID_FIELD_FOR_INSERT_UPDATE-like rejection", err)
	}
}

func TestContractLifecycleTransitionRulesFollowMetadataAuthority(t *testing.T) {
	ctx := context.Background()
	conn, sm := setupCRMRuntimeForContractLifecycle(t, ctx)
	defer conn.Close()

	admin := systemAdminSession()
	statusField := findFieldByAPIName(t, sm, ctx, "Contract", "Status")
	if statusField.Validator == nil || *statusField.Validator != "state_transition" {
		t.Fatalf("Contract.Status validator = %#v, want state_transition metadata", statusField.Validator)
	}
	if statusField.ValidatorConfig == nil || *statusField.ValidatorConfig == "" {
		t.Fatal("Contract.Status missing validatorConfig metadata")
	}

	accountID := createAccountForOrderActivation(t, ctx, sm, admin)
	contractID, _ := createDraftContractForLifecycle(t, ctx, sm, admin, accountID)
	if err := sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"Status": "Activated",
	}, admin); err != nil {
		t.Fatalf("Draft -> Activated: %v", err)
	}
	if err := sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"Status": "Expired",
	}, admin); err != nil {
		t.Fatalf("Activated -> Expired: %v", err)
	}

	err := sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"ContractTerm": 24,
	}, admin)
	if err == nil || !strings.Contains(strings.ToLower(err.Error()), "lock") {
		t.Fatalf("post-activation lock error = %v, want metadata-enforced lock rejection", err)
	}
}

func TestContractLifecycleActivationStampFollowsFlowAuthority(t *testing.T) {
	ctx := context.Background()
	conn, sm := setupCRMRuntimeForContractLifecycle(t, ctx)
	defer conn.Close()

	admin := systemAdminSession()
	disableFlowByName(t, ctx, sm, "Std_ContractActivateStamp")

	accountID := createAccountForOrderActivation(t, ctx, sm, admin)
	contractID, _ := createDraftContractForLifecycle(t, ctx, sm, admin, accountID)
	if err := sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"Status": "Activated",
	}, admin); err != nil {
		t.Fatalf("activate contract with flow disabled: %v", err)
	}

	contract, err := sm.Persistence().GetRaw(ctx, "Contract", contractID)
	if err != nil {
		t.Fatalf("get contract with flow disabled: %v", err)
	}
	if got := contract["Status"]; got != "Activated" {
		t.Fatalf("Contract.Status = %#v, want Activated", got)
	}
	if got := contract["StatusCode"]; got == "Activated" {
		t.Fatalf("Contract.StatusCode = %#v, want unstamped value when flow disabled", got)
	}
	if got := contract["ActivatedById"]; got != nil && got != "" {
		t.Fatalf("Contract.ActivatedById = %#v, want no stamp when flow disabled", got)
	}
	if got := contract["ActivatedDate"]; got != nil && got != "" {
		t.Fatalf("Contract.ActivatedDate = %#v, want no stamp when flow disabled", got)
	}

	err = sm.Persistence().Update(ctx, "Contract", contractID, models.SObject{
		"ContractTerm": 24,
	}, admin)
	if err == nil || !strings.Contains(strings.ToLower(err.Error()), "lock") {
		t.Fatalf("flow-disabled lifecycle lock error = %v, want metadata lock rejection", err)
	}
}

func createDraftContractForLifecycle(t *testing.T, ctx context.Context, sm ServiceGraph, user *models.UserSession, accountID string) (string, string) {
	t.Helper()
	contract, err := sm.Persistence().Insert(ctx, "Contract", models.SObject{
		"AccountId":    accountID,
		"Status":       "Draft",
		"StartDate":    "2026-01-01",
		"EndDate":      "2026-12-31",
		"ContractTerm": 12,
	}, user)
	if err != nil {
		t.Fatalf("create draft contract: %v", err)
	}
	id, _ := contract["Id"].(string)
	if id == "" {
		t.Fatal("create draft contract returned empty Id")
	}
	number, _ := contract["ContractNumber"].(string)
	return id, number
}

func setupCRMRuntimeForContractLifecycle(t *testing.T, ctx context.Context) (*database.TiDBConnection, ServiceGraph) {
	t.Helper()

	cfg, cleanup := newTemporaryContractLifecycleDatabase(t)
	t.Cleanup(cleanup)
	setDatabaseEnvFromConfig(t, cfg)

	conn := openIntegrationConnection(t, cfg)
	if err := InitL0(ctx, conn); err != nil {
		_ = conn.Close()
		t.Fatalf("init-l0: %v", err)
	}
	_ = conn.Close()

	conn = openIntegrationConnection(t, cfg)
	r, err := NewRunner(conn)
	if err != nil {
		t.Fatalf("new runner: %v", err)
	}
	if err := r.RefreshCache(ctx); err != nil {
		t.Fatalf("initial cache refresh: %v", err)
	}
	if err := ApplyPacks(ctx, r, false); err != nil {
		t.Fatalf("apply runtime packs: %v", err)
	}
	t.Setenv(packs.OfficialAppPacksRootEnvVar, officialPackRootFromControlplaneTest(t))
	if err := InstallPackFromSource(ctx, r, "official_repo:salesforce-crm-standard"); err != nil {
		t.Fatalf("install CRM pack: %v", err)
	}

	// Reuse the Runner's service graph (built via the public BuildServiceGraph
	// seam) as the record-setup graph; it is constructed from the same
	// infrastructure deps a standalone ServiceManager would be.
	sm := r.SvcMgr
	if err := sm.RefreshMetadataCache(ctx); err != nil {
		t.Fatalf("runtime metadata refresh: %v", err)
	}
	return conn, sm
}

func newTemporaryContractLifecycleDatabase(t *testing.T) (*mysql.Config, func()) {
	t.Helper()

	host := os.Getenv("TIDB_HOST")
	if host == "" {
		host = "127.0.0.1"
	}
	port := os.Getenv("TIDB_PORT")
	if port == "" {
		port = "4000"
	}
	user := os.Getenv("TIDB_USER")
	if user == "" {
		user = "root"
	}

	cfg := &mysql.Config{
		User:                 user,
		Passwd:               os.Getenv("TIDB_PASSWORD"),
		Net:                  "tcp",
		Addr:                 host + ":" + port,
		Collation:            "utf8mb4_unicode_ci",
		ParseTime:            true,
		Loc:                  time.Local,
		AllowNativePasswords: true,
	}
	if host != "127.0.0.1" && host != "localhost" {
		tlsName := fmt.Sprintf("tidb_contract_lifecycle_%d", time.Now().UnixNano())
		if err := mysql.RegisterTLSConfig(tlsName, &tls.Config{
			MinVersion: tls.VersionTLS12,
			ServerName: host,
		}); err == nil {
			cfg.TLSConfig = tlsName
		}
	}

	adminDB, err := sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		t.Fatalf("open admin TiDB connection: %v", err)
	}
	if err := adminDB.Ping(); err != nil {
		_ = adminDB.Close()
		t.Skipf("skipping integration test: TiDB is not reachable at %s:%s: %v", host, port, err)
	}

	dbName := fmt.Sprintf("nexuscrm_contract_lifecycle_%d", time.Now().UnixNano())
	if _, err := adminDB.Exec("CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); err != nil {
		_ = adminDB.Close()
		t.Skipf("skipping integration test: cannot create temporary database %s: %v", dbName, err)
	}

	testCfg := *cfg
	testCfg.DBName = dbName
	cleanup := func() {
		_, _ = adminDB.Exec("DROP DATABASE IF EXISTS `" + dbName + "`")
		_ = adminDB.Close()
	}
	return &testCfg, cleanup
}

func findFieldByAPIName(t *testing.T, sm ServiceGraph, ctx context.Context, objectName, fieldName string) *models.FieldMetadata {
	t.Helper()
	schema := sm.Metadata().GetSchema(ctx, objectName)
	if schema == nil {
		t.Fatalf("schema %s not found", objectName)
	}
	for i := range schema.Fields {
		if schema.Fields[i].APIName == fieldName {
			return &schema.Fields[i]
		}
	}
	t.Fatalf("field %s.%s not found", objectName, fieldName)
	return nil
}
