package tenantoverlay

import (
	"context"
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/models"
)

// stubMetaRepoTFR implements ports.MetadataRepository for the tenant flow
// reconcile path via interface embedding. Only flow-related methods are
// instrumented; the MetadataService RefreshCache surface below fails closed
// with errStub, and every other method resolves through the embedded
// interface, so an unexpected call panics instead of silently passing.
type stubMetaRepoTFR struct {
	ports.MetadataRepository

	getDefByName     func(ctx context.Context, name string) (*models.FlowDefinition, error)
	ensureDefByDev   func(ctx context.Context, def *models.FlowDefinition) (*models.FlowDefinition, error)
	lockDefByDev     func(ctx context.Context, name string) (*models.FlowDefinition, error)
	getFlowVersion   func(ctx context.Context, id string) (*models.FlowVersion, error)
	getFlowVersions  func(ctx context.Context, defID string) ([]*models.FlowVersion, error)
	lockFlowVersions func(ctx context.Context, defID string) ([]*models.FlowVersion, error)
	createFlowVer    func(ctx context.Context, ver *models.FlowVersion) error
	activateFlowVer  func(ctx context.Context, id string) error
	updateDef        func(ctx context.Context, id string, def *models.FlowDefinition) error
}

var errStub = fmt.Errorf("stubMetaRepoTFR: unimplemented method called")

// RefreshCache fail-closed surface. buildAndSwapCache (the RefreshCache body
// in the pinned nexuscrm-metadata) calls these six methods unconditionally:
// GetAllSchemas and GetAllRelationships via buildCoreSnapshot, then
// GetAllFlowDefinitions, GetAllValidationRules, GetAllAutoNumbers, and
// GetAllIdentifierRules. stubMetaRepoTFR is embedded by repos that back a
// real MetadataService (filterFormulaRepo, tenantFlowRuntimeRepo), so each of
// these must return a descriptive error rather than dispatch through the nil
// embedded interface, which would panic. tenantFlowRuntimeRepo's runtime
// maintenance tests additionally rely on the schema-load failure being
// tolerated (logged, then skipped).
// TestStubMetaRepoTFRRefreshSurfaceFailsClosed guards this set.
func (s *stubMetaRepoTFR) GetAllSchemas(ctx context.Context) ([]*models.ObjectMetadata, error) {
	return nil, errStub
}
func (s *stubMetaRepoTFR) GetAllRelationships(ctx context.Context) ([]*models.Relationship, error) {
	return nil, errStub
}
func (s *stubMetaRepoTFR) GetAllFlowDefinitions(ctx context.Context) ([]*models.FlowDefinition, error) {
	return nil, errStub
}
func (s *stubMetaRepoTFR) GetAllValidationRules(ctx context.Context) ([]*models.ValidationRule, error) {
	return nil, errStub
}
func (s *stubMetaRepoTFR) GetAllAutoNumbers(ctx context.Context) ([]*models.AutoNumber, error) {
	return nil, errStub
}
func (s *stubMetaRepoTFR) GetAllIdentifierRules(ctx context.Context) ([]*models.IdentifierRule, error) {
	return nil, errStub
}

// TestStubMetaRepoTFRRefreshSurfaceFailsClosed guards the invariant above:
// every method MetadataService.RefreshCache calls unconditionally must return
// the descriptive errStub instead of panicking through the nil embedded
// interface. Deleting one of the overrides fails this test with the panic.
func TestStubMetaRepoTFRRefreshSurfaceFailsClosed(t *testing.T) {
	stub := &stubMetaRepoTFR{}
	for _, name := range []string{
		"GetAllSchemas",
		"GetAllRelationships",
		"GetAllFlowDefinitions",
		"GetAllValidationRules",
		"GetAllAutoNumbers",
		"GetAllIdentifierRules",
	} {
		t.Run(name, func(t *testing.T) {
			defer func() {
				if r := recover(); r != nil {
					t.Fatalf("stubMetaRepoTFR.%s panicked (%v): RefreshCache calls it unconditionally, keep a fail-closed override", name, r)
				}
			}()
			out := reflect.ValueOf(stub).MethodByName(name).Call([]reflect.Value{reflect.ValueOf(context.Background())})
			err, _ := out[len(out)-1].Interface().(error)
			if err == nil || !strings.Contains(err.Error(), "stubMetaRepoTFR") {
				t.Fatalf("stubMetaRepoTFR.%s error = %v, want errStub", name, err)
			}
		})
	}
}
func (s *stubMetaRepoTFR) GetFlowDefinitionByName(ctx context.Context, name string) (*models.FlowDefinition, error) {
	if s.getDefByName != nil {
		return s.getDefByName(ctx, name)
	}
	return nil, errStub
}
func (s *stubMetaRepoTFR) LockFlowDefinitionByDeveloperNameForUpdate(ctx context.Context, name string) (*models.FlowDefinition, error) {
	if s.lockDefByDev != nil {
		return s.lockDefByDev(ctx, name)
	}
	return nil, errStub
}
func (s *stubMetaRepoTFR) EnsureFlowDefinitionByDeveloperName(ctx context.Context, def *models.FlowDefinition) (*models.FlowDefinition, error) {
	if s.ensureDefByDev != nil {
		return s.ensureDefByDev(ctx, def)
	}
	return nil, errStub
}
func (s *stubMetaRepoTFR) UpdateFlowDefinition(ctx context.Context, id string, def *models.FlowDefinition) error {
	if s.updateDef != nil {
		return s.updateDef(ctx, id, def)
	}
	return errStub
}
func (s *stubMetaRepoTFR) GetFlowVersions(ctx context.Context, definitionId string) ([]*models.FlowVersion, error) {
	if s.getFlowVersions != nil {
		return s.getFlowVersions(ctx, definitionId)
	}
	return nil, errStub
}
func (s *stubMetaRepoTFR) GetFlowVersionsForUpdate(ctx context.Context, definitionId string) ([]*models.FlowVersion, error) {
	if s.lockFlowVersions != nil {
		return s.lockFlowVersions(ctx, definitionId)
	}
	if s.getFlowVersions != nil {
		return s.getFlowVersions(ctx, definitionId)
	}
	return nil, errStub
}
func (s *stubMetaRepoTFR) GetFlowVersion(ctx context.Context, id string) (*models.FlowVersion, error) {
	if s.getFlowVersion != nil {
		return s.getFlowVersion(ctx, id)
	}
	return nil, errStub
}
func (s *stubMetaRepoTFR) CreateTenantFlowVersion(ctx context.Context, version *models.FlowVersion) error {
	if s.createFlowVer != nil {
		return s.createFlowVer(ctx, version)
	}
	return errStub
}
func (s *stubMetaRepoTFR) ActivateFlowVersion(ctx context.Context, id string) error {
	if s.activateFlowVer != nil {
		return s.activateFlowVer(ctx, id)
	}
	return errStub
}

func TestReconcileTenantFlow_SkipsCustomized(t *testing.T) {
	updateCalled := false
	repo := &stubMetaRepoTFR{
		getDefByName: func(ctx context.Context, name string) (*models.FlowDefinition, error) {
			return &models.FlowDefinition{
				Id:            "def1",
				DeveloperName: name,
				IsCustomized:  false,
			}, nil
		},
		lockDefByDev: func(ctx context.Context, name string) (*models.FlowDefinition, error) {
			return &models.FlowDefinition{
				Id:            "def1",
				DeveloperName: name,
				IsCustomized:  true,
			}, nil
		},
		updateDef: func(ctx context.Context, id string, def *models.FlowDefinition) error {
			updateCalled = true
			return nil
		},
	}
	deps := packs.ApplyDeps{MetadataRepo: repo}

	err := reconcileTenantFlow(context.Background(), deps, "Test_Flow", []byte(`{"label":"Test Flow"}`))
	if err != nil {
		t.Fatalf("reconcileTenantFlow() error = %v", err)
	}
	if updateCalled {
		t.Fatal("UpdateFlowDefinition was called for locked customized row")
	}
}

func TestReconcileTenantFlow_FirstCreateStakeOut(t *testing.T) {
	repo := &stubMetaRepoTFR{
		getDefByName: func(ctx context.Context, name string) (*models.FlowDefinition, error) {
			return nil, nil
		},
		ensureDefByDev: func(ctx context.Context, def *models.FlowDefinition) (*models.FlowDefinition, error) {
			return &models.FlowDefinition{
				Id:            "def_new",
				DeveloperName: def.DeveloperName,
				IsCustomized:  false,
			}, nil
		},
		getFlowVersion: func(ctx context.Context, id string) (*models.FlowVersion, error) {
			return nil, nil
		},
		getFlowVersions: func(ctx context.Context, defID string) ([]*models.FlowVersion, error) {
			return nil, nil
		},
		createFlowVer: func(ctx context.Context, ver *models.FlowVersion) error {
			return nil
		},
		activateFlowVer: func(ctx context.Context, id string) error {
			return nil
		},
		updateDef: func(ctx context.Context, id string, def *models.FlowDefinition) error {
			if def.MasterLabel != "Test Flow" {
				t.Fatalf("UpdateFlowDefinition MasterLabel = %q, want %q", def.MasterLabel, "Test Flow")
			}
			return nil
		},
	}

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT KeyPrefix FROM _System_Object WHERE ApiName = \\?").
		WithArgs("FlowDefinition").
		WillReturnRows(sqlmock.NewRows([]string{"KeyPrefix"}).AddRow("300"))
	mock.ExpectQuery("SELECT KeyPrefix FROM _System_Object WHERE ApiName = \\?").
		WithArgs("Flow").
		WillReturnRows(sqlmock.NewRows([]string{"KeyPrefix"}).AddRow("301"))

	deps := packs.ApplyDeps{
		MetadataRepo: repo,
		DB:           db,
	}

	err = reconcileTenantFlow(context.Background(), deps, "Test_Flow", []byte(`{"label":"Test Flow"}`))
	if err != nil {
		t.Fatalf("reconcileTenantFlow() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestReconcileTenantFlow_DefinitionDisappearedAfterStakeOut(t *testing.T) {
	repo := &stubMetaRepoTFR{
		getDefByName: func(ctx context.Context, name string) (*models.FlowDefinition, error) {
			return nil, nil
		},
		ensureDefByDev: func(ctx context.Context, def *models.FlowDefinition) (*models.FlowDefinition, error) {
			return nil, nil
		},
	}

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	// newTenantFlowID needs key prefix for FlowDefinition.
	mock.ExpectQuery("SELECT KeyPrefix FROM _System_Object WHERE ApiName = \\?").
		WithArgs("FlowDefinition").
		WillReturnRows(sqlmock.NewRows([]string{"KeyPrefix"}).AddRow("300"))

	deps := packs.ApplyDeps{
		MetadataRepo: repo,
		DB:           db,
	}

	err = reconcileTenantFlow(context.Background(), deps, "Race_Flow", []byte(`{"label":"Race Flow"}`))
	if err == nil {
		t.Fatal("reconcileTenantFlow() = nil, want 'disappeared' error")
	}
	if !strings.Contains(err.Error(), "disappeared") {
		t.Fatalf("error = %v, want 'disappeared'", err)
	}
}

func TestReconcileTenantFlow_StakeOutLoserCustomizedSkips(t *testing.T) {
	updateCalled := false
	repo := &stubMetaRepoTFR{
		getDefByName: func(ctx context.Context, name string) (*models.FlowDefinition, error) {
			return nil, nil
		},
		ensureDefByDev: func(ctx context.Context, def *models.FlowDefinition) (*models.FlowDefinition, error) {
			return &models.FlowDefinition{
				Id:            "def_existing",
				DeveloperName: def.DeveloperName,
				IsCustomized:  true,
			}, nil
		},
		updateDef: func(ctx context.Context, id string, def *models.FlowDefinition) error {
			updateCalled = true
			return nil
		},
	}

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT KeyPrefix FROM _System_Object WHERE ApiName = \\?").
		WithArgs("FlowDefinition").
		WillReturnRows(sqlmock.NewRows([]string{"KeyPrefix"}).AddRow("300"))

	deps := packs.ApplyDeps{
		MetadataRepo: repo,
		DB:           db,
	}

	err = reconcileTenantFlow(context.Background(), deps, "Race_Customized", []byte(`{"label":"Race Customized"}`))
	if err != nil {
		t.Fatalf("reconcileTenantFlow() error = %v", err)
	}
	if updateCalled {
		t.Fatal("UpdateFlowDefinition was called for customized stake-out winner")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestReconcileTenantFlow_ExistingDefinitionActivatesNewVersion(t *testing.T) {
	repo := &stubMetaRepoTFR{
		getDefByName: func(ctx context.Context, name string) (*models.FlowDefinition, error) {
			return &models.FlowDefinition{
				Id:            "def_existing",
				DeveloperName: "Existing_Flow",
				IsCustomized:  false,
			}, nil
		},
		lockDefByDev: func(ctx context.Context, name string) (*models.FlowDefinition, error) {
			return &models.FlowDefinition{
				Id:              "def_existing",
				DeveloperName:   name,
				ActiveVersionId: "ver_old",
				IsCustomized:    false,
			}, nil
		},
		getFlowVersion: func(ctx context.Context, id string) (*models.FlowVersion, error) {
			t.Fatalf("GetFlowVersion was called; active row should come from locked version set")
			return nil, nil
		},
		lockFlowVersions: func(ctx context.Context, defID string) ([]*models.FlowVersion, error) {
			return []*models.FlowVersion{{
				Id:            "ver_old",
				VersionNumber: 1,
				ProcessType:   "AutoLaunchedFlow",
				MetadataRaw:   []byte(`{"label":"Old Label"}`),
			}}, nil
		},
		createFlowVer: func(ctx context.Context, ver *models.FlowVersion) error {
			if ver.VersionNumber != 2 {
				t.Fatalf("CreateFlowVersion VersionNumber = %d, want 2", ver.VersionNumber)
			}
			return nil
		},
		activateFlowVer: func(ctx context.Context, id string) error {
			return nil
		},
		updateDef: func(ctx context.Context, id string, def *models.FlowDefinition) error {
			return nil
		},
	}

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT KeyPrefix FROM _System_Object WHERE ApiName = \\?").
		WithArgs("Flow").
		WillReturnRows(sqlmock.NewRows([]string{"KeyPrefix"}).AddRow("301"))

	deps := packs.ApplyDeps{
		MetadataRepo: repo,
		DB:           db,
	}

	err = reconcileTenantFlow(context.Background(), deps, "Existing_Flow", []byte(`{"label":"New Label"}`))
	if err != nil {
		t.Fatalf("reconcileTenantFlow() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestReconcileTenantFlow_RejectsMalformedJSON(t *testing.T) {
	deps := packs.ApplyDeps{}

	err := reconcileTenantFlow(context.Background(), deps, "Bad_Flow", []byte(`{not json`))
	if err == nil {
		t.Fatal("reconcileTenantFlow() = nil, want parse error")
	}
	if !strings.Contains(err.Error(), "canonicalize") && !strings.Contains(err.Error(), "parse") {
		t.Fatalf("error = %v, want canonicalize/parse detail", err)
	}
}

func TestReconcileTenantFlow_RejectsUnsupportedAtomicTriggerShape(t *testing.T) {
	deps := packs.ApplyDeps{}

	tests := []struct {
		name string
		raw  []byte
	}{
		{
			name: "record after-save trigger",
			raw: []byte(`{
				"label":"Bad Flow",
				"runAtomically":true,
				"start":{"triggerType":"RecordAfterSave","object":"ProbeObject"}
			}`),
		},
		{
			name: "workflow-derived after-save trigger",
			raw: []byte(`{
				"label":"Bad Workflow Flow",
				"processType":"Workflow",
				"runAtomically":true,
				"variables":[{"name":"myVariable_current","dataType":"SObject","objectType":"Lead"}]
			}`),
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := reconcileTenantFlow(context.Background(), deps, "Bad_Flow", tc.raw)
			if err == nil {
				t.Fatal("reconcileTenantFlow() error = nil, want unsupported atomic trigger rejection")
			}
			if !strings.Contains(err.Error(), `triggerType "RecordAfterSave" is unsupported`) {
				t.Fatalf("error = %v, want unsupported trigger detail", err)
			}
		})
	}
}
