package runtimeassembly

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net"
	"os"
	"reflect"
	"testing"
	"time"

	"github.com/go-sql-driver/mysql"
	database "github.com/nexuscrm/db"
	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestPageLayoutRoleAuthorityOnTiDB(t *testing.T) {
	for _, root := range []struct {
		name   string
		parent any
	}{{"null", nil}, {"empty", ""}} {
		t.Run(root.name, func(t *testing.T) { testPageLayoutRoleAuthorityOnTiDB(t, root.parent) })
	}
}

func testPageLayoutRoleAuthorityOnTiDB(t *testing.T, rootParent any) {
	ctx, cancel := context.WithTimeout(t.Context(), 45*time.Second)
	defer cancel()
	db, writer := newLayoutRuntimeTestDB(t, ctx, constants.TableUserRole, "_System_RuntimeSecurityState", constants.TableObject, constants.TableField, constants.TablePageLayout, constants.TableLayoutAssignment)
	exec := func(t *testing.T, statement string, args ...any) {
		t.Helper()
		if _, err := writer.ExecContext(ctx, statement, args...); err != nil {
			t.Fatal(err)
		}
	}
	exec(t, `INSERT INTO _System_RuntimeSecurityState (OrgId,RuntimeAudience,SecurityEpoch) VALUES ('org-a','runtime',3)`)
	// This fixture deliberately represents pre-existing/imported rows that
	// can contain an empty root despite the fresh schema's parent foreign key.
	// Disable checks only on the dedicated fixture connection, then restore them.
	seedConn, err := writer.Conn(ctx)
	if err != nil {
		t.Fatal(err)
	}
	defer seedConn.Close()
	if _, err := seedConn.ExecContext(ctx, "SET FOREIGN_KEY_CHECKS=0"); err != nil {
		t.Fatal(err)
	}
	if _, err := seedConn.ExecContext(ctx, `INSERT INTO UserRole (Id,Name,ParentRoleId,IsDeleted) VALUES ('old-parent','Old',?,0),('new-parent','New',?,0),('child','Child','old-parent',0)`, rootParent, rootParent); err != nil {
		t.Fatal(err)
	}
	if _, err := seedConn.ExecContext(ctx, "SET FOREIGN_KEY_CHECKS=1"); err != nil {
		t.Fatal(err)
	}
	exec(t, `INSERT INTO _System_Object (Id,ApiName,TableType,Label,SharingModel) VALUES ('object-a','WorkItem','standard_object','Work item','Private')`)
	for _, parent := range []string{"old-parent", "new-parent"} {
		raw, err := json.Marshal(models.PageLayout{ID: "layout-" + parent, ObjectAPIName: "WorkItem", LayoutName: parent, Type: "Detail", Source: "admin_created", IsCustomized: true})
		if err != nil {
			t.Fatal(err)
		}
		exec(t, `INSERT INTO _System_PageLayout (Id,ObjectApiName,LayoutName,Config,Source,IsCustomized) VALUES (?,'WorkItem',?,?,'admin_created',1)`, "layout-"+parent, parent, raw)
		exec(t, `INSERT INTO _System_LayoutAssignment (Id,ObjectApiName,LayoutId,RoleId,Specificity,RouteSource) VALUES (?,'WorkItem',?,?,1,'admin_authored')`, "assignment-"+parent, "layout-"+parent, parent)
	}
	permissionService := permissions.NewPermissionService(persistence.NewPermissionRepository(db), nil, nil)
	runtime, err := NewPageLayoutRuntime(database.NewConnectionFromDB(db), permissionService)
	if err != nil {
		t.Fatal(err)
	}
	check := func(epoch uint64, want string) {
		t.Helper()
		ancestors, err := runtime.Engine.GetRoleAncestorsAtSecurityEpoch(ctx, "org-a", "CHILD", epoch)
		if err != nil {
			t.Fatal(err)
		}
		if !reflect.DeepEqual(ancestors, []string{want}) {
			t.Fatalf("ancestors=%v want=%s", ancestors, want)
		}
		layouts, err := runtime.Engine.ResolveCurrentPageLayouts(ctx, "WorkItem", "", append([]string{"child"}, ancestors...), []string{""})
		if err != nil {
			t.Fatal(err)
		}
		if layouts[""].ID != "layout-"+want {
			t.Fatalf("selected Layout=%s want=layout-%s", layouts[""].ID, want)
		}
	}
	check(3, "old-parent")
	exec(t, `UPDATE UserRole SET ParentRoleId='new-parent' WHERE Id='child'`)
	check(3, "new-parent") // Role writes can change current data without an epoch bump.
	if got := permissionService.GetRoleAncestors("child"); len(got) == 0 || got[0] != "old-parent" {
		t.Fatalf("test did not retain the old process cache: %v", got)
	}
	exec(t, `UPDATE _System_RuntimeSecurityState SET SecurityEpoch=4 WHERE OrgId='org-a'`)
	check(4, "new-parent")
	if ancestors, err := runtime.Engine.GetRoleAncestorsAtSecurityEpoch(ctx, "org-a", "new-parent", 4); err != nil || len(ancestors) != 0 {
		t.Fatalf("root role ancestors=%v err=%v", ancestors, err)
	}
	for _, test := range []struct {
		name, org, role string
		epoch           uint64
	}{
		{"stale epoch", "org-a", "child", 3}, {"future epoch", "org-a", "child", 5}, {"wrong org", "org-b", "child", 4}, {"missing role", "org-a", "missing", 4},
	} {
		t.Run(test.name, func(t *testing.T) {
			if _, err := runtime.Engine.GetRoleAncestorsAtSecurityEpoch(ctx, test.org, test.role, test.epoch); err == nil {
				t.Fatal("invalid authority accepted")
			}
		})
	}
	for _, test := range []struct{ name, statement string }{
		{"deleted role", `UPDATE UserRole SET IsDeleted=1 WHERE Id='child'`},
		{"deleted parent", `UPDATE UserRole SET IsDeleted=1 WHERE Id='new-parent'`},
		{"cycle", `UPDATE UserRole SET ParentRoleId='child' WHERE Id='new-parent'`},
	} {
		t.Run(test.name, func(t *testing.T) {
			exec(t, `UPDATE UserRole SET IsDeleted=0,ParentRoleId=NULL`)
			exec(t, `UPDATE UserRole SET ParentRoleId='new-parent' WHERE Id='child'`)
			exec(t, test.statement)
			if _, err := runtime.Engine.GetRoleAncestorsAtSecurityEpoch(ctx, "org-a", "child", 4); err == nil {
				t.Fatal("invalid hierarchy accepted")
			}
		})
	}
	canceled, stop := context.WithCancel(ctx)
	stop()
	if _, err := runtime.Engine.GetRoleAncestorsAtSecurityEpoch(canceled, "org-a", "child", 4); err == nil {
		t.Fatal("canceled query accepted")
	}
}

func newLayoutRuntimeTestDB(t *testing.T, ctx context.Context, tables ...string) (*sql.DB, *sql.DB) {
	t.Helper()
	if os.Getenv("TIDB_HOST") == "" && os.Getenv("NEXUSCRM_TIDB_REQUIRED") != "1" {
		t.Skip("requires TiDB")
	}
	host, port, user := os.Getenv("TIDB_HOST"), os.Getenv("TIDB_PORT"), os.Getenv("TIDB_USER")
	if host == "" {
		host = "127.0.0.1"
	}
	if port == "" {
		port = "4001"
	}
	if user == "" {
		user = "root"
	}
	cfg := mysql.NewConfig()
	cfg.User = user
	cfg.Passwd = os.Getenv("TIDB_PASSWORD")
	cfg.Net = "tcp"
	cfg.Addr = net.JoinHostPort(host, port)
	cfg.ParseTime = true
	admin, err := sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { admin.Close() })
	name := fmt.Sprintf("nexuscrm_layout_runtime_%d", time.Now().UnixNano())
	if _, err := admin.ExecContext(ctx, "CREATE DATABASE `"+name+"`"); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { admin.ExecContext(context.Background(), "DROP DATABASE `"+name+"`") })
	cfg.DBName = name
	db, err := sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	// A separate pool represents the other Runtime replica; it never refreshes
	// the reader process's already warm PermissionService cache.
	writer, err := sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { writer.Close() })
	wanted := make(map[string]bool)
	for _, name := range tables {
		wanted[name] = true
	}
	definitions, err := schemacatalog.ManagedTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	schema := persistence.NewSchemaRepository(db, nil)
	for _, definition := range definitions {
		if wanted[definition.TableName] {
			if err := schema.CreatePhysicalTable(ctx, definition); err != nil {
				t.Fatal(err)
			}
			delete(wanted, definition.TableName)
		}
	}
	if len(wanted) > 0 {
		t.Fatalf("missing canonical tables: %v", wanted)
	}

	return db, writer
}
