package tenantoverlay

import (
	"context"
	"database/sql"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/backend/internal/controlplane/packs"
	"github.com/nexuscrm/backend/internal/infrastructure/persistence"
)

func TestCanonicalizeFlowMetadata_StableAcrossWhitespaceAndKeyOrder(t *testing.T) {
	a := []byte(`{"label":"X","description":"d","status":"Active"}`)
	b := []byte(`{
		"status": "Active",
		"description": "d",
		"label": "X"
	}`)

	ca, err := canonicalizeFlowMetadata(a)
	if err != nil {
		t.Fatalf("canonicalize a: %v", err)
	}
	cb, err := canonicalizeFlowMetadata(b)
	if err != nil {
		t.Fatalf("canonicalize b: %v", err)
	}
	if string(ca) != string(cb) {
		t.Errorf("canonical output diverged:\n a=%s\n b=%s", ca, cb)
	}
}

func TestExtractFlowProcessType_DefaultsToAutoLaunched(t *testing.T) {
	cases := []struct {
		in   []byte
		want string
	}{
		{[]byte(`{"processType":"Workflow"}`), "Workflow"},
		{[]byte(`{"processType":""}`), "AutoLaunchedFlow"},
		{[]byte(`{}`), "AutoLaunchedFlow"},
		{[]byte(`not even json`), "AutoLaunchedFlow"},
	}
	for _, c := range cases {
		if got := extractFlowProcessType(c.in); got != c.want {
			t.Errorf("extractFlowProcessType(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

func TestFirstNonEmpty(t *testing.T) {
	if got := firstNonEmpty("", "", "third"); got != "third" {
		t.Errorf("got %q", got)
	}
	if got := firstNonEmpty("first", "second"); got != "first" {
		t.Errorf("got %q", got)
	}
	if got := firstNonEmpty(); got != "" {
		t.Errorf("got %q, want empty", got)
	}
}

func TestNewTenantFlowID_FlowDefinitionUses300Prefix(t *testing.T) {
	id := newTenantFlowID("FlowDefinition")
	if !strings.HasPrefix(id, "300") {
		t.Errorf("newTenantFlowID(FlowDefinition) = %q, want prefix 300", id)
	}
}

func TestNewTenantFlowID_FlowUses301Prefix(t *testing.T) {
	id := newTenantFlowID("Flow")
	if !strings.HasPrefix(id, "301") {
		t.Errorf("newTenantFlowID(Flow) = %q, want prefix 301", id)
	}
}

func TestNewTenantFlowID_UnknownObjectPanics(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Error("newTenantFlowID with unknown object must panic")
		}
	}()
	newTenantFlowID("Unknown__c")
}

func TestReconcileTenantFlow_CustomizedDefinition_SkipsReconcile(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	repo := persistence.NewMetadataRepository(db)

	rows := sqlmock.NewRows([]string{
		"Id", "DeveloperName", "MasterLabel", "Description", "ProcessType",
		"TriggerType", "TriggerObjectOrEvent", "ActiveVersionId", "LatestVersionId", "IsDeleted", "IsCustomized",
		"CreatedDate", "LastModifiedDate",
	}).AddRow(
		"fd_existing", "Lead_StatusChangeTimestamp", "Admin Hand Flow", "", "AutoLaunchedFlow",
		"RecordBeforeSave", "Lead", "", "", false, true,
		"", "",
	)
	mock.ExpectQuery(`SELECT .* FROM FlowDefinition WHERE DeveloperName = \?`).
		WithArgs("Lead_StatusChangeTimestamp").
		WillReturnRows(rows)

	err = reconcileTenantFlow(
		context.Background(),
		packs.ApplyDeps{MetadataRepo: repo},
		"Lead_StatusChangeTimestamp",
		[]byte(`{"label":"x"}`),
	)
	if err != nil {
		t.Fatalf("reconcileTenantFlow: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unexpected extra SQL: %v", err)
	}
}

func TestReconcileTenantFlow_FreshSeed_ClaimsOwnershipAndSyncsProjection(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	repo := persistence.NewMetadataRepository(db)

	mock.ExpectQuery(`SELECT .* FROM FlowDefinition WHERE DeveloperName = \?`).
		WithArgs("Lead_StatusChangeTimestamp").
		WillReturnError(sql.ErrNoRows)

	mock.ExpectExec(`INSERT INTO FlowDefinition`).
		WithArgs(
			sqlmock.AnyArg(), "Lead_StatusChangeTimestamp", "",
			"", "", "", "", nil, nil,
			false, false, sqlmock.AnyArg(), sqlmock.AnyArg(),
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	mock.ExpectQuery(`SELECT .* FROM Flow WHERE DefinitionId = \? ORDER BY VersionNumber DESC`).
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "DefinitionId", "MasterLabel", "VersionNumber", "Status", "Description",
			"ProcessType", "Metadata", "Source", "IsDeleted", "CreatedDate", "LastModifiedDate",
		}))

	mock.ExpectExec(`(?s)INSERT INTO Flow.*'custom'.*`).
		WithArgs(
			sqlmock.AnyArg(), sqlmock.AnyArg(), 1, "Draft",
			"", "AutoLaunchedFlow", sqlmock.AnyArg(),
			sqlmock.AnyArg(), sqlmock.AnyArg(),
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	expectActivateFlowVersion(mock)

	mock.ExpectExec(`UPDATE FlowDefinition SET DeveloperName = \?`).
		WillReturnResult(sqlmock.NewResult(1, 1))

	err = reconcileTenantFlow(
		context.Background(),
		packs.ApplyDeps{MetadataRepo: repo},
		"Lead_StatusChangeTimestamp",
		[]byte(`{"label":"Lead Status Change Timestamp","start":{"triggerType":"RecordBeforeSave","object":"Lead"}}`),
	)
	if err != nil {
		t.Fatalf("reconcileTenantFlow: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("ExpectationsWereMet: %v", err)
	}
}

func TestReconcileTenantFlow_ReclaimsMatchingStandardSourceVersion(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	repo := persistence.NewMetadataRepository(db)

	raw := []byte(`{"label":"Lead Status Change Timestamp","start":{"triggerType":"RecordBeforeSave","object":"Lead"}}`)

	mock.ExpectQuery(`SELECT .* FROM FlowDefinition WHERE DeveloperName = \?`).
		WithArgs("Lead_StatusChangeTimestamp").
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "DeveloperName", "MasterLabel", "Description", "ProcessType",
			"TriggerType", "TriggerObjectOrEvent", "ActiveVersionId", "LatestVersionId", "IsDeleted", "IsCustomized",
			"CreatedDate", "LastModifiedDate",
		}).AddRow(
			"fd_existing", "Lead_StatusChangeTimestamp", "Lead Status Change Timestamp", "", "AutoLaunchedFlow",
			"RecordBeforeSave", "Lead", "fv_standard", "fv_standard", false, false,
			"", "",
		))

	mock.ExpectQuery(`SELECT Id, DefinitionId, MasterLabel, VersionNumber, Status, Description, ProcessType, Metadata,\s+Source, IsDeleted, CreatedDate, LastModifiedDate FROM Flow WHERE Id = \?`).
		WithArgs("fv_standard").
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "DefinitionId", "MasterLabel", "VersionNumber", "Status", "Description",
			"ProcessType", "Metadata", "Source", "IsDeleted", "CreatedDate", "LastModifiedDate",
		}).AddRow(
			"fv_standard", "fd_existing", "Lead Status Change Timestamp", 1, "Active", "",
			"AutoLaunchedFlow", string(raw), "standard", false, "", "",
		))

	mock.ExpectQuery(`SELECT .* FROM Flow WHERE DefinitionId = \? ORDER BY VersionNumber DESC`).
		WithArgs("fd_existing").
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "DefinitionId", "MasterLabel", "VersionNumber", "Status", "Description",
			"ProcessType", "Metadata", "Source", "IsDeleted", "CreatedDate", "LastModifiedDate",
		}).AddRow(
			"fv_standard", "fd_existing", "Lead Status Change Timestamp", 1, "Active", "",
			"AutoLaunchedFlow", string(raw), "standard", false, "", "",
		))

	mock.ExpectExec(`(?s)INSERT INTO Flow.*'custom'.*`).
		WithArgs(
			sqlmock.AnyArg(), "fd_existing", 2, "Draft",
			"", "AutoLaunchedFlow", sqlmock.AnyArg(),
			sqlmock.AnyArg(), sqlmock.AnyArg(),
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	expectActivateFlowVersion(mock)

	mock.ExpectExec(`UPDATE FlowDefinition SET DeveloperName = \?`).
		WillReturnResult(sqlmock.NewResult(1, 1))

	err = reconcileTenantFlow(
		context.Background(),
		packs.ApplyDeps{MetadataRepo: repo},
		"Lead_StatusChangeTimestamp",
		raw,
	)
	if err != nil {
		t.Fatalf("reconcileTenantFlow: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("ExpectationsWereMet: %v", err)
	}
}

func TestSoftDeleteMissingTenantFlows_OnlyTouchesOwnedRows(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	repo := persistence.NewMetadataRepository(db)
	retiredTenantFlowDeveloperNames = []string{"Removed_Flow"}
	defer func() { retiredTenantFlowDeveloperNames = nil }()

	rows := sqlmock.NewRows([]string{
		"Id", "DeveloperName", "MasterLabel", "Description", "ProcessType",
		"TriggerType", "TriggerObjectOrEvent", "ActiveVersionId", "LatestVersionId", "IsDeleted", "IsCustomized",
		"CreatedDate", "LastModifiedDate",
	}).AddRow(
		"fd_missing", "Removed_Flow", "Removed Flow", "", "AutoLaunchedFlow",
		"", "", "", "", false, false, "", "",
	)
	mock.ExpectQuery(`SELECT .* FROM FlowDefinition WHERE DeveloperName = \?`).
		WithArgs("Removed_Flow").
		WillReturnRows(rows)

	mock.ExpectExec(`UPDATE FlowDefinition SET DeveloperName = \?`).
		WithArgs(
			"Removed_Flow", "Removed Flow", "", "AutoLaunchedFlow",
			"", "", nil, nil, true, sqlmock.AnyArg(), "fd_missing",
		).
		WillReturnResult(sqlmock.NewResult(0, 1))

	err = softDeleteMissingTenantFlows(
		context.Background(),
		packs.ApplyDeps{MetadataRepo: repo},
		map[string]struct{}{"Still_There": {}},
	)
	if err != nil {
		t.Fatalf("softDeleteMissingTenantFlows: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("ExpectationsWereMet: %v", err)
	}
}

func expectActivateFlowVersion(mock sqlmock.Sqlmock) {
	mock.ExpectQuery(`SELECT DefinitionId FROM Flow WHERE Id = \?`).
		WithArgs(sqlmock.AnyArg()).
		WillReturnRows(sqlmock.NewRows([]string{"DefinitionId"}).AddRow("def-id"))
	mock.ExpectExec(`UPDATE Flow SET Status = 'Obsolete' WHERE DefinitionId = \? AND Status = 'Active'`).
		WithArgs("def-id").
		WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectExec(`UPDATE Flow SET Status = 'Active' WHERE Id = \?`).
		WithArgs(sqlmock.AnyArg()).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`UPDATE FlowDefinition SET ActiveVersionId = \?, IsDeleted = 0 WHERE Id = \?`).
		WithArgs(sqlmock.AnyArg(), "def-id").
		WillReturnResult(sqlmock.NewResult(0, 1))
}

