package dependencyretry

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/go-sql-driver/mysql"
	commonv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func TestClassificationFailsClosedAcrossJoinedCauses(t *testing.T) {
	unavailable := &commonv1.Failure{Category: commonv1.FailureUnavailable}
	permanent := errors.New("identity conflict")
	for _, tc := range []struct {
		name  string
		err   error
		retry bool
	}{
		{"unavailable", unavailable, true},
		{"deadline", context.DeadlineExceeded, true},
		{"deadlock", &mysql.MySQLError{Number: 1213}, true},
		{"connection", driver.ErrBadConn, true},
		{"wrapped", fmt.Errorf("RPC: %w", unavailable), true},
		{"joined transient", errors.Join(unavailable, driver.ErrBadConn), true},
		{"already rolled back", errors.Join(context.DeadlineExceeded, sql.ErrTxDone), true},
		{"transaction misuse", sql.ErrTxDone, false},
		{"permanent", permanent, false},
		{"mixed join", errors.Join(permanent, unavailable), false},
		{"nested mixed join", fmt.Errorf("owner: %w", errors.Join(unavailable, errors.Join(permanent, driver.ErrBadConn))), false},
		{"missing table", &mysql.MySQLError{Number: 1146}, false},
		{"access denied", &commonv1.Failure{Category: commonv1.FailureForbidden}, false},
		{"internal unknown", &commonv1.Failure{Category: commonv1.FailureInternal}, false},
		{"unknown", errors.New("temporarily unavailable"), false},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if got := transientReason(tc.err) != ""; got != tc.retry {
				t.Fatalf("retry=%t want %t", got, tc.retry)
			}
		})
	}
}

func TestTransientRecoveryAndParentCancellation(t *testing.T) {
	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
	defer cancel()
	calls := 0
	if err := do(ctx, "test", func() error {
		calls++
		if calls < 4 {
			return &commonv1.Failure{Category: commonv1.FailureUnavailable}
		}
		return nil
	}, time.Millisecond, 2*time.Millisecond); err != nil || calls != 4 {
		t.Fatalf("calls=%d err=%v", calls, err)
	}
	ctx2, cancel2 := context.WithCancel(t.Context())
	calls = 0
	err := do(ctx2, "canceled", func() error { calls++; cancel2(); return context.DeadlineExceeded }, time.Hour, time.Hour)
	if !errors.Is(err, context.Canceled) || calls != 1 {
		t.Fatalf("calls=%d err=%v", calls, err)
	}
}

func TestCancellationInterruptsBackoff(t *testing.T) {
	ctx, cancel := context.WithCancel(t.Context())
	entered := make(chan struct{})
	done := make(chan error, 1)
	go func() {
		done <- do(ctx, "backoff", func() error { close(entered); return driver.ErrBadConn }, time.Hour, time.Hour)
	}()
	<-entered
	cancel()
	select {
	case err := <-done:
		if !errors.Is(err, context.Canceled) {
			t.Fatal(err)
		}
	case <-time.After(time.Second):
		t.Fatal("backoff ignored cancellation")
	}
}
