package automation

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

	"github.com/go-sql-driver/mysql"
)

func TestScheduledRunReservationKey_RejectsMissingWindowToken(t *testing.T) {
	_, err := NewScheduledRunReservationKey("def-1", "ver-1", "", "rec-1")
	if err == nil {
		t.Fatal("NewScheduledRunReservationKey missing window token = nil, want error")
	}
}

func TestScheduledRunReservationKeyRejectsIdentityTruncation(t *testing.T) {
	_, err := NewScheduledRunReservationKey(strings.Repeat("x", 192), "ver-1", "window-1", "rec-1")
	if err == nil || !strings.Contains(err.Error(), "durable column bound") {
		t.Fatalf("NewScheduledRunReservationKey oversized identity error = %v", err)
	}
}

func TestScheduledRunReservationKey_SameWindowSameInputCollides(t *testing.T) {
	keyA, err := NewScheduledRunReservationKey("def-1", "ver-1", "2026-05-13T10:00Z", "rec-1")
	if err != nil {
		t.Fatalf("keyA err = %v", err)
	}
	keyB, err := NewScheduledRunReservationKey("def-1", "ver-1", "2026-05-13T10:00Z", "rec-1")
	if err != nil {
		t.Fatalf("keyB err = %v", err)
	}
	keyC, err := NewScheduledRunReservationKey("def-1", "ver-1", "2026-05-13T11:00Z", "rec-1")
	if err != nil {
		t.Fatalf("keyC err = %v", err)
	}

	if keyA.CanonicalID() != keyB.CanonicalID() {
		t.Fatalf("same window/input canonical ids differ: %q vs %q", keyA.CanonicalID(), keyB.CanonicalID())
	}
	if keyA.CanonicalID() == keyC.CanonicalID() {
		t.Fatalf("different window tokens canonicalized to same id: %q", keyA.CanonicalID())
	}
}

func TestScheduledRunIdentityDigestSeparatesComponentBoundariesAndDomains(t *testing.T) {
	if scheduledRunIdentityDigest("ab", "c") == scheduledRunIdentityDigest("a", "bc") {
		t.Fatal("identity digest lost component boundaries")
	}
	key, err := NewScheduledRunReservationKey("def", "ver", "window", "input")
	if err != nil {
		t.Fatal(err)
	}
	if legacyScheduledRunOccurrenceIdentityDigest(key) ==
		scheduledRunIdentityDigest(
			"scoped", "", "", "",
			key.FlowDefinitionID, key.FlowVersionID, key.WindowToken, key.InputKey,
		) {
		t.Fatal("legacy and scoped identity domains collided")
	}
}

func TestScheduledRunReservationService_ReserveRejectsBypassedConstructorKey(t *testing.T) {
	svc := NewScheduledRunReservationService(&sql.DB{})

	_, err := svc.Reserve(nil, ScheduledRunReservationKey{
		FlowDefinitionID: "def-1",
		FlowVersionID:    "ver-1",
	})
	if err == nil {
		t.Fatal("Reserve with missing window token = nil, want error")
	}
}

func TestIsScheduledRunReservationDuplicateErr_MySQLDuplicate(t *testing.T) {
	err := errors.New("wrapped reserve failure: " + (&mysql.MySQLError{
		Number:  1062,
		Message: "Duplicate entry 'def-1' for key 'uq_scheduled_run_reservation_identity'",
	}).Error())
	if !isScheduledRunReservationDuplicateErr(err) {
		t.Fatal("wrapped mysql 1062 error = false, want true")
	}
}

func TestIsScheduledRunReservationDuplicateErr_RejectsGenericUniqueText(t *testing.T) {
	err := errors.New("temporary unique network partition while reserving row")
	if isScheduledRunReservationDuplicateErr(err) {
		t.Fatal("generic error containing unique text = true, want false")
	}
}
