package permissionstatewritergen

import (
	"go/ast"
	"go/parser"
	"go/token"
	"slices"
	"sort"
	"testing"
)

func TestMutationTablesDetectsExactMutationTargets(t *testing.T) {
	tableConstants := map[string]string{
		"TableApp":                  "_System_App",
		"TableProfileAppVisibility": "ProfileAppVisibility",
		"TableRecordType":           "_System_RecordType",
		"TableUser":                 "User",
	}
	targets := map[string]string{
		"_System_App":          "app-surface",
		"ProfileAppVisibility": "app-visibility-surface",
		"_System_RecordType":   "record-type-surface",
		"User":                 "",
	}
	tests := []struct {
		name   string
		body   string
		tables []string
	}{
		{
			name:   "query builder mutation",
			body:   `query.Update(constants.TableApp).Set(values)`,
			tables: []string{"_System_App"},
		},
		{
			name:   "bulk query builder mutation",
			body:   `query.BulkInsertOrdered(constants.TableApp, columns, records, nil)`,
			tables: []string{"_System_App"},
		},
		{
			name: "query builder table alias",
			body: `tableName := constants.TableApp
				query.Delete(tableName).Where("Id", id)`,
			tables: []string{"_System_App"},
		},
		{
			name:   "non-surface cascade parent mutation target",
			body:   `query.Delete(constants.TableUser).Where("Id", id)`,
			tables: []string{"User"},
		},
		{
			name: "formatted target does not classify protected read source",
			body: `fmt.Sprintf(
				"INSERT IGNORE INTO %s (Id) SELECT app.Id FROM %s app",
				quoted(constants.TableProfileAppVisibility),
				quoted(constants.TableApp),
			)`,
			tables: []string{"ProfileAppVisibility"},
		},
		{
			name: "concatenated formatted SQL",
			body: `fmt.Sprintf(
				"INSERT INTO " + "%s" + " (Id) VALUES (?) ON DUPLICATE KEY UPDATE Id = Id",
				constants.TableApp,
			)`,
			tables: []string{"_System_App"},
		},
		{
			name: "raw SQL through local binding",
			body: `statement := "UPDATE ` + "`_System_RecordType`" + ` SET IsActive = 1 WHERE Id = ?"
				exec.ExecContext(ctx, statement, id)`,
			tables: []string{"_System_RecordType"},
		},
		{
			name: "locking read is not a mutation",
			body: `exec.QueryContext(ctx, "SELECT Id FROM ` + "`_System_RecordType`" + ` FOR UPDATE")`,
		},
		{
			name: "longer table name is not app mutation",
			body: "exec.ExecContext(ctx, \"UPDATE `_System_ApprovalProcess` SET IsActive = 0\")",
		},
		{
			name: "unknown table is ignored",
			body: `query.Delete(constants.TableOther).Where("Id", id)`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			body := parseFunctionBody(t, tt.body)
			got := mutationTables(body, tableConstants, targets, nil)
			sort.Strings(tt.tables)
			if !slices.Equal(got, tt.tables) {
				t.Fatalf("mutation tables = %v, want %v", got, tt.tables)
			}
		})
	}
}

func TestLiteralMutationOperationsTreatsDestructiveReplacementAndDDLAsDeletes(t *testing.T) {
	for _, statement := range []string{
		"REPLACE INTO `_System_App` (`Id`) VALUES (?)",
		"TRUNCATE TABLE `_System_App`",
		"DROP TABLE IF EXISTS `_System_App`",
	} {
		operations := literalMutationOperations(statement, "_System_App")
		if operations&mutationDelete == 0 {
			t.Errorf("%q operations = %b, want delete semantics", statement, operations)
		}
	}
}

func TestAffectedSurfaceIDsExpandsOnlyDeleteCascades(t *testing.T) {
	catalog := managedCatalog{
		surfaces: map[string]string{
			"Parent":     "surface-parent",
			"Child":      "surface-child",
			"Grandchild": "surface-grandchild",
		},
		cascadeChildren: map[string][]string{
			"Parent": {"Child"},
			"Child":  {"Grandchild"},
		},
	}
	if got := affectedSurfaceIDs(
		map[string]mutationOperation{"Parent": mutationUpdate},
		catalog,
	); !slices.Equal(got, []string{"surface-parent"}) {
		t.Fatalf("update surfaces = %v, want parent only", got)
	}
	if got := affectedSurfaceIDs(
		map[string]mutationOperation{"Parent": mutationDelete},
		catalog,
	); !slices.Equal(got, []string{"surface-child", "surface-grandchild", "surface-parent"}) {
		t.Fatalf("delete surfaces = %v, want transitive cascade closure", got)
	}
}

func TestAffectedSurfaceIDsExpandsFromNonSurfaceCascadeParent(t *testing.T) {
	catalog := managedCatalog{
		surfaces: map[string]string{
			"Assignment":      "surface-assignment",
			"GroupAssignment": "surface-group-assignment",
		},
		cascadeChildren: map[string][]string{
			"User": {"Assignment", "GroupAssignment"},
		},
	}
	if got := affectedSurfaceIDs(
		map[string]mutationOperation{"User": mutationDelete},
		catalog,
	); !slices.Equal(got, []string{"surface-assignment", "surface-group-assignment"}) {
		t.Fatalf("delete surfaces = %v, want non-surface parent cascade closure", got)
	}
}

func parseFunctionBody(t *testing.T, body string) *ast.BlockStmt {
	t.Helper()
	source := "package example\nfunc target() {\n" + body + "\n}\n"
	file, err := parser.ParseFile(token.NewFileSet(), "example.go", source, 0)
	if err != nil {
		t.Fatal(err)
	}
	return file.Decls[0].(*ast.FuncDecl).Body
}
