package audit

import (
	"encoding/json"
	"errors"
	"fmt"
	"testing"

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

func TestClassifyError_Nil(t *testing.T) {
	if got := ClassifyError(nil); got != PoisonClassNone {
		t.Fatalf("ClassifyError(nil) = %q, want %q", got, PoisonClassNone)
	}
}

func TestClassifyError_JSONSyntax(t *testing.T) {
	// Trigger a real *json.SyntaxError.
	var v map[string]string
	err := json.Unmarshal([]byte(`{"broken":`), &v)
	if err == nil {
		t.Fatal("expected JSON parse error")
	}
	if got := ClassifyError(err); got != PoisonClassParseFail {
		t.Fatalf("got %q, want %q", got, PoisonClassParseFail)
	}

	// Wrapped.
	wrapped := fmt.Errorf("worker step: %w", err)
	if got := ClassifyError(wrapped); got != PoisonClassParseFail {
		t.Fatalf("wrapped: got %q, want %q", got, PoisonClassParseFail)
	}
}

func TestClassifyError_JSONUnmarshalType(t *testing.T) {
	var v int
	err := json.Unmarshal([]byte(`"not-an-int"`), &v)
	if err == nil {
		t.Fatal("expected JSON type error")
	}
	if got := ClassifyError(err); got != PoisonClassParseFail {
		t.Fatalf("got %q, want %q", got, PoisonClassParseFail)
	}
}

func TestClassifyError_FKViolation_Codes(t *testing.T) {
	for _, code := range []uint16{1451, 1452} {
		err := &mysql.MySQLError{Number: code, Message: "foreign key constraint fails"}
		if got := ClassifyError(err); got != PoisonClassFKViolation {
			t.Fatalf("code %d: got %q, want %q", code, got, PoisonClassFKViolation)
		}
	}
}

func TestClassifyError_FKViolation_WrappedMessage(t *testing.T) {
	err := errors.New("some wrapper: Error 1452: Cannot add or update a child row: foreign key constraint fails")
	if got := ClassifyError(err); got != PoisonClassFKViolation {
		t.Fatalf("got %q, want %q", got, PoisonClassFKViolation)
	}
}

func TestClassifyError_TransientLock(t *testing.T) {
	for _, code := range []uint16{1205, 1213} {
		err := &mysql.MySQLError{Number: code, Message: "transient"}
		if got := ClassifyError(err); got != PoisonClassNone {
			t.Fatalf("code %d: got %q, want %q (transient must not be poisoned)",
				code, got, PoisonClassNone)
		}
	}
}

func TestClassifyError_DataTruncated_IsParseFail(t *testing.T) {
	err := &mysql.MySQLError{Number: 1265, Message: "Data truncated"}
	if got := ClassifyError(err); got != PoisonClassParseFail {
		t.Fatalf("got %q, want %q", got, PoisonClassParseFail)
	}
}

func TestClassifyError_Unknown(t *testing.T) {
	err := errors.New("something bizarre happened")
	if got := ClassifyError(err); got != PoisonClassUnknown {
		t.Fatalf("got %q, want %q", got, PoisonClassUnknown)
	}
}

func TestClassifyError_NeverReturnsRetryExhausted(t *testing.T) {
	// Contract: RETRY_EXHAUSTED is a worker-assigned terminal, not an error
	// shape. ClassifyError must never return it regardless of input.
	inputs := []error{
		nil,
		errors.New("boom"),
		&mysql.MySQLError{Number: 1213},
		&mysql.MySQLError{Number: 1452},
	}
	for _, err := range inputs {
		if got := ClassifyError(err); got == PoisonClassRetryExhausted {
			t.Fatalf("ClassifyError returned RETRY_EXHAUSTED for %v", err)
		}
	}
}
