//go:build integration

package applicationnavigation

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

	"github.com/go-sql-driver/mysql"
	database "github.com/nexuscrm/db"
	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	"github.com/nexuscrm/persistence"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationnavigationv1"
)

func navigationDatabase(t *testing.T) *sql.DB {
	t.Helper()
	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 = host + ":" + port
	cfg.ParseTime = true
	cfg.Collation = "utf8mb4_unicode_ci"
	cfg.Timeout = 5 * time.Second
	admin, err := sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		t.Fatal(err)
	}
	if err := admin.PingContext(t.Context()); err != nil {
		admin.Close()
		if os.Getenv("NEXUSCRM_TIDB_REQUIRED") == "1" {
			t.Fatal(err)
		}
		t.Skipf("TiDB unavailable: %v", err)
	}
	name := fmt.Sprintf("navigation_initialization_%d", time.Now().UnixNano())
	if _, err := admin.ExecContext(t.Context(), "CREATE DATABASE `"+name+"`"); err != nil {
		admin.Close()
		t.Fatal(err)
	}
	cfg.DBName = name
	raw, err := sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { raw.Close(); admin.Exec("DROP DATABASE `" + name + "`"); admin.Close() })
	definitions, err := schemacatalog.SystemTableDefinitions()
	if err != nil {
		t.Fatal(err)
	}
	schema := persistence.NewSchemaRepository(raw, nil)
	for _, definition := range definitions {
		if definition.TableName == "_System_App" || definition.TableName == "_System_ApplicationNavigationReceipt" {
			if err := schema.CreatePhysicalTable(t.Context(), definition); err != nil {
				t.Fatal(err)
			}
		}
	}
	return raw
}

func TestSeedOnceAndExplicitLegacyMigrationOnTiDB(t *testing.T) {
	ctx, cancel := context.WithTimeout(t.Context(), 45*time.Second)
	defer cancel()
	raw := navigationDatabase(t)
	connection := database.NewConnectionFromDB(raw)
	template := []semantic.Item{{ID: "start", Label: "Start", Target: semantic.Target{Page: &semantic.PageTarget{ID: "start"}}}}
	add := func(id string, items any) {
		t.Helper()
		if _, err := raw.ExecContext(t.Context(), "INSERT INTO `_System_App` (Id,Name,Label,FrontendAppId,Icon,Color,NavigationItems,UtilityItems,AssignedProfiles) VALUES (?,?,?,'owner','','',?,JSON_ARRAY(),JSON_ARRAY())", id, id, id, items); err != nil {
			t.Fatal(err)
		}
	}
	input := func(id string) Input {
		return Input{Schema: Schema, OwnerID: "owner", ApplicationID: id, Template: template}
	}
	add("new", "null")
	seeded, err := Apply(t.Context(), connection, input("new"), false)
	if err != nil || seeded.Disposition != "initialized" || !reflect.DeepEqual(seeded.Document.Items, template) {
		t.Fatalf("seed=%+v err=%v", seeded, err)
	}
	add("empty", "[]")
	empty, err := Apply(t.Context(), connection, input("empty"), false)
	if err != nil || empty.Disposition != "migrated" || empty.Document.Items == nil || len(empty.Document.Items) != 0 {
		t.Fatalf("empty=%+v err=%v", empty, err)
	}
	changed := input("empty")
	changed.Template = []semantic.Item{}
	preserved, err := Apply(t.Context(), connection, changed, false)
	if err != nil || preserved.Disposition != "preserved" || preserved.Document.Revision != 1 || len(preserved.Document.Items) != 0 {
		t.Fatalf("preserve=%+v err=%v", preserved, err)
	}
	legacy := `[{"id":"link","type":"web","label":"Edited","pageUrl":"https://example.com/current?q=1#anchor","icon":"Book"}]`
	add("legacy", legacy)
	if _, err := Apply(t.Context(), connection, input("legacy"), false); err == nil || !strings.Contains(err.Error(), "explicit legacy") {
		t.Fatalf("missing migration=%v", err)
	}
	var observed []byte
	if err := raw.QueryRow("SELECT NavigationItems FROM `_System_App` WHERE Id='legacy'").Scan(&observed); err != nil {
		t.Fatal(err)
	}
	link := "https://example.com/current?q=1#anchor"
	migration := input("legacy")
	migration.Migration = &Migration{SourceDigest: persistence.NavigationSourceDigest(observed), Items: []semantic.Item{{ID: "link", Label: "Edited", Icon: "Book", Target: semantic.Target{URL: &link}}}}
	migrated, err := Apply(t.Context(), connection, migration, false)
	if err != nil || !reflect.DeepEqual(migrated.Document.Items, migration.Migration.Items) {
		t.Fatalf("migration=%+v err=%v", migrated, err)
	}
	// An edit through the Runtime storage owner survives all later installation templates.
	manager := persistence.NewTransactionManager(connection)
	repository := persistence.NewApplicationNavigationRepository(raw, manager)
	if err := manager.WithPessimisticTransaction(ctx, func(ctx context.Context) error {
		before, err := repository.Lock(ctx, "owner", "new")
		if err != nil {
			return err
		}
		_, err = repository.Store(ctx, "owner", "admin", semantic.SaveRequest{ApplicationID: "new", ExpectedRevision: before.Revision, IdempotencyKey: "clear", Items: []semantic.Item{}}, before)
		return err
	}); err != nil {
		t.Fatal(err)
	}
	after, err := Apply(t.Context(), connection, input("new"), false)
	if err != nil || after.Document.Revision != 2 || len(after.Document.Items) != 0 {
		t.Fatalf("upgrade erased edit: %+v %v", after, err)
	}
	assert, err := Apply(t.Context(), connection, input("new"), true)
	if err != nil || !reflect.DeepEqual(assert.Document, after.Document) {
		t.Fatalf("assert=%+v err=%v", assert, err)
	}
	add("stale", legacy)
	stale := input("stale")
	stale.Migration = migration.Migration
	if _, err := raw.Exec("UPDATE `_System_App` SET NavigationItems=JSON_ARRAY(JSON_OBJECT('changed',TRUE)) WHERE Id='stale'"); err != nil {
		t.Fatal(err)
	}
	if _, err := Apply(t.Context(), connection, stale, false); !errors.Is(err, persistence.ErrNavigationConflict) {
		t.Fatalf("stale migration=%v", err)
	}
	var revision uint64
	if err := raw.QueryRow("SELECT NavigationRevision FROM `_System_App` WHERE Id='stale'").Scan(&revision); err != nil || revision != 0 {
		t.Fatalf("failed migration changed revision %d: %v", revision, err)
	}
}

func TestInitializationDecodingRejectsNullTemplates(t *testing.T) {
	for _, value := range []string{`null`, `{}`, `{"schema":"nexuscrm.application-navigation-initialization/v1","owner_id":"owner","application_id":"app","template":null}`} {
		if _, err := DecodeInput(strings.NewReader(value)); err == nil {
			t.Fatalf("accepted %s", value)
		}
	}
	raw, _ := json.Marshal(Input{Schema: Schema, OwnerID: "owner", ApplicationID: "app", Template: []semantic.Item{}})
	if _, err := DecodeInput(strings.NewReader(string(raw))); err != nil {
		t.Fatal(err)
	}
}
