package bootstrapimpl

import (
	"database/sql/driver"
	"encoding/json"
	"regexp"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	domainschema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

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

	selectLive := regexp.QuoteMeta("SELECT `AccessPolicy`, `AccessPolicyRecordMutationPackName`, `AccessPolicyLifecyclePackName`, `AccessPolicyRecordMutationPackValue`, `AccessPolicyLifecyclePackValue` FROM `_System_Object` WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND COALESCE(`IsDeleted`, 0) = 0 LIMIT 1")
	mock.ExpectQuery(selectLive).
		WithArgs("recordtype", string(constants.TableTypeStandardObject)).
		WillReturnRows(sqlmock.NewRows([]string{"AccessPolicy", "AccessPolicyRecordMutationPackName", "AccessPolicyLifecyclePackName", "AccessPolicyRecordMutationPackValue", "AccessPolicyLifecyclePackValue"}).
			AddRow(`{"recordMutation":{"deny":[{"surfaces":["singleCreate"],"message":"closed"}]}}`, "salesforce-crm-standard", "", nil, nil))

	update := regexp.QuoteMeta("UPDATE `_System_Object` SET `AccessPolicy` = CAST(? AS JSON), `LastModifiedDate` = NOW() WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND (`AccessPolicy` IS NULL OR CAST(`AccessPolicy` AS CHAR) != ?)")
	mock.ExpectExec(update).
		WithArgs(
			accessPolicyArg{
				t:                  t,
				wantMode:           "derived",
				wantRecordMutation: true,
			},
			"recordtype",
			string(constants.TableTypeStandardObject),
			accessPolicyArg{
				t:                  t,
				wantMode:           "derived",
				wantRecordMutation: true,
			},
		).
		WillReturnResult(sqlmock.NewResult(0, 1))

	err = reconcileManagedStandardObjectAccessPolicyJSON(db, []domainschema.TableDefinition{{
		TableName:    "RecordType",
		TableType:    string(constants.TableTypeStandardObject),
		AccessPolicy: &models.ObjectAccessPolicy{Mode: "derived"},
	}})
	if err != nil {
		t.Fatalf("reconcileManagedStandardObjectAccessPolicyJSON: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	selectLive := regexp.QuoteMeta("SELECT `AccessPolicy`, `AccessPolicyRecordMutationPackName`, `AccessPolicyLifecyclePackName`, `AccessPolicyRecordMutationPackValue`, `AccessPolicyLifecyclePackValue` FROM `_System_Object` WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND COALESCE(`IsDeleted`, 0) = 0 LIMIT 1")
	mock.ExpectQuery(selectLive).
		WithArgs("recordtype", string(constants.TableTypeStandardObject)).
		WillReturnRows(sqlmock.NewRows([]string{"AccessPolicy", "AccessPolicyRecordMutationPackName", "AccessPolicyLifecyclePackName", "AccessPolicyRecordMutationPackValue", "AccessPolicyLifecyclePackValue"}).
			AddRow(`{"mode":"derived"}`, "salesforce-crm-standard", "", `{"deny":[{"surfaces":["singleCreate"],"message":"closed"}]}`, nil))

	update := regexp.QuoteMeta("UPDATE `_System_Object` SET `AccessPolicy` = CAST(? AS JSON), `LastModifiedDate` = NOW() WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND (`AccessPolicy` IS NULL OR CAST(`AccessPolicy` AS CHAR) != ?)")
	mock.ExpectExec(update).
		WithArgs(
			accessPolicyArg{
				t:                  t,
				wantMode:           "derived",
				wantRecordMutation: true,
			},
			"recordtype",
			string(constants.TableTypeStandardObject),
			accessPolicyArg{
				t:                  t,
				wantMode:           "derived",
				wantRecordMutation: true,
			},
		).
		WillReturnResult(sqlmock.NewResult(0, 1))

	err = reconcileManagedStandardObjectAccessPolicyJSON(db, []domainschema.TableDefinition{{
		TableName:    "RecordType",
		TableType:    string(constants.TableTypeStandardObject),
		AccessPolicy: &models.ObjectAccessPolicy{Mode: "derived"},
	}})
	if err != nil {
		t.Fatalf("reconcileManagedStandardObjectAccessPolicyJSON: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	selectLive := regexp.QuoteMeta("SELECT `AccessPolicy`, `AccessPolicyRecordMutationPackName`, `AccessPolicyLifecyclePackName`, `AccessPolicyRecordMutationPackValue`, `AccessPolicyLifecyclePackValue` FROM `_System_Object` WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND COALESCE(`IsDeleted`, 0) = 0 LIMIT 1")
	mock.ExpectQuery(selectLive).
		WithArgs("profile", string(constants.TableTypeSystemCore)).
		WillReturnRows(sqlmock.NewRows([]string{"AccessPolicy", "AccessPolicyRecordMutationPackName", "AccessPolicyLifecyclePackName", "AccessPolicyRecordMutationPackValue", "AccessPolicyLifecyclePackValue"}).
			AddRow(`null`, nil, nil, nil, nil))

	update := regexp.QuoteMeta("UPDATE `_System_Object` SET `AccessPolicy` = CAST(? AS JSON), `LastModifiedDate` = NOW() WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND (`AccessPolicy` IS NULL OR CAST(`AccessPolicy` AS CHAR) != ?)")
	mock.ExpectExec(update).
		WithArgs(
			accessPolicyArg{
				t:             t,
				wantLifecycle: true,
			},
			"profile",
			string(constants.TableTypeSystemCore),
			accessPolicyArg{
				t:             t,
				wantLifecycle: true,
			},
		).
		WillReturnResult(sqlmock.NewResult(0, 1))

	err = reconcileManagedStandardObjectAccessPolicyJSON(db, []domainschema.TableDefinition{{
		TableName: "Profile",
		TableType: string(constants.TableTypeSystemCore),
		AccessPolicy: &models.ObjectAccessPolicy{
			Lifecycle: &models.RecordLifecycleAccessPolicy{
				Hooks: []models.RecordLifecycleHookPolicy{{
					Phase:      "after_create_in_tx",
					Capability: "platform.profile-permission-carrier",
				}},
			},
		},
	}})
	if err != nil {
		t.Fatalf("reconcileManagedStandardObjectAccessPolicyJSON: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	selectLive := regexp.QuoteMeta("SELECT `AccessPolicy`, `AccessPolicyRecordMutationPackName`, `AccessPolicyLifecyclePackName`, `AccessPolicyRecordMutationPackValue`, `AccessPolicyLifecyclePackValue` FROM `_System_Object` WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND COALESCE(`IsDeleted`, 0) = 0 LIMIT 1")
	mock.ExpectQuery(selectLive).
		WithArgs("processdefinition", string(constants.TableTypeStandardObject)).
		WillReturnRows(sqlmock.NewRows([]string{"AccessPolicy", "AccessPolicyRecordMutationPackName", "AccessPolicyLifecyclePackName", "AccessPolicyRecordMutationPackValue", "AccessPolicyLifecyclePackValue"}).
			AddRow(`{"lifecycle":{"hooks":[{"phase":"after_create_in_tx","capability":"crm.approval-process-child-sync"}]}}`, "", "salesforce-crm-standard", nil, nil))

	update := regexp.QuoteMeta("UPDATE `_System_Object` SET `AccessPolicy` = CAST(? AS JSON), `LastModifiedDate` = NOW() WHERE LOWER(`ApiName`) = ? AND `TableType` = ? AND (`AccessPolicy` IS NULL OR CAST(`AccessPolicy` AS CHAR) != ?)")
	mock.ExpectExec(update).
		WithArgs(
			accessPolicyArg{
				t:             t,
				wantMode:      "derived",
				wantLifecycle: true,
			},
			"processdefinition",
			string(constants.TableTypeStandardObject),
			accessPolicyArg{
				t:             t,
				wantMode:      "derived",
				wantLifecycle: true,
			},
		).
		WillReturnResult(sqlmock.NewResult(0, 1))

	err = reconcileManagedStandardObjectAccessPolicyJSON(db, []domainschema.TableDefinition{{
		TableName:    "ProcessDefinition",
		TableType:    string(constants.TableTypeStandardObject),
		AccessPolicy: &models.ObjectAccessPolicy{Mode: "derived"},
	}})
	if err != nil {
		t.Fatalf("reconcileManagedStandardObjectAccessPolicyJSON: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

type accessPolicyArg struct {
	t                  *testing.T
	wantMode           string
	wantRecordMutation bool
	wantLifecycle      bool
}

func (a accessPolicyArg) Match(value driver.Value) bool {
	raw, ok := value.(string)
	if !ok {
		a.t.Logf("AccessPolicy arg type = %T, want string", value)
		return false
	}
	var policy models.ObjectAccessPolicy
	if err := json.Unmarshal([]byte(raw), &policy); err != nil {
		a.t.Logf("AccessPolicy arg is not JSON: %v raw=%s", err, raw)
		return false
	}
	if policy.Mode != a.wantMode {
		a.t.Logf("AccessPolicy mode = %q, want %q; raw=%s", policy.Mode, a.wantMode, raw)
		return false
	}
	if (policy.RecordMutation != nil) != a.wantRecordMutation {
		a.t.Logf("AccessPolicy recordMutation present = %v, want %v; raw=%s", policy.RecordMutation != nil, a.wantRecordMutation, raw)
		return false
	}
	if (policy.Lifecycle != nil) != a.wantLifecycle {
		a.t.Logf("AccessPolicy lifecycle present = %v, want %v; raw=%s", policy.Lifecycle != nil, a.wantLifecycle, raw)
		return false
	}
	return true
}
