package checker

import (
	"context"
	"errors"
	"io"
	"net"
	"syscall"
	"testing"

	"github.com/stretchr/testify/assert"
)

// timeoutOnlyError is a minimal net.Error that reports Timeout()=true.
// Used to drive the net.Error.Timeout() branch in classifyError.
type timeoutOnlyError struct{}

func (timeoutOnlyError) Error() string   { return "i/o timeout" }
func (timeoutOnlyError) Timeout() bool   { return true }
func (timeoutOnlyError) Temporary() bool { return true }

func TestClassifyError(t *testing.T) {
	cases := []struct {
		name string
		err  error
		want Classification
	}{
		{"nil", nil, ClassificationPermanent},
		{"context_canceled", context.Canceled, ClassificationPermanent},
		{"context_deadline", context.DeadlineExceeded, ClassificationPermanent},
		{"econnrefused", syscall.ECONNREFUSED, ClassificationPermanent},
		{"econnreset", syscall.ECONNRESET, ClassificationTransient},
		{"econnreset_wrapped", &net.OpError{Op: "read", Err: syscall.ECONNRESET}, ClassificationTransient},
		{"net_timeout", &timeoutOnlyError{}, ClassificationTransient},
		{"dns_temporary", &net.DNSError{IsTemporary: true}, ClassificationTransient},
		{"dns_timeout", &net.DNSError{IsTimeout: true}, ClassificationTransient},
		{"dns_permanent", &net.DNSError{IsNotFound: true}, ClassificationPermanent},
		{"socks5_auth_rejected", ErrAuthRejected, ClassificationPermanent},
		{"socks5_bad_version", ErrSocks5BadVersion, ClassificationPermanent},
		{"socks5_rejected_all_auth", ErrSocks5RejectedAllAuth, ClassificationPermanent},
		{"socks5_credential_too_long", ErrCredentialTooLong, ClassificationPermanent},
		{"socks5_host_too_long", ErrHostTooLong, ClassificationPermanent},
		{"socks5_unsupported_relay_atyp", ErrUnsupportedRelayATYP, ClassificationPermanent},
		{"socks5_short_reply", ErrShortReply, ClassificationPermanent},
		{"socks5_reply_general_failure", ErrSocks5Reply{Code: 0x01}, ClassificationPermanent},
		{"socks5_reply_not_allowed", ErrSocks5Reply{Code: 0x02}, ClassificationPermanent},
		{"socks5_reply_refused", ErrSocks5Reply{Code: 0x05}, ClassificationPermanent},
		{"socks5_reply_unsupported", ErrSocks5Reply{Code: 0x07}, ClassificationPermanent},
		{"stun_too_short", ErrSTUNTooShort, ClassificationPermanent},
		{"stun_bad_magic", ErrSTUNBadMagicCookie, ClassificationPermanent},
		{"stun_not_success", ErrSTUNNotSuccess, ClassificationPermanent},
		{"stun_txid_mismatch", ErrSTUNTxIDMismatch, ClassificationPermanent},
		{"stun_no_mapped", ErrSTUNNoMappedAddress, ClassificationPermanent},
		{"stun_unsupported_family", ErrSTUNUnsupportedFamily, ClassificationPermanent},
		{"unexpected_eof", io.ErrUnexpectedEOF, ClassificationPermanent},
		{"unexpected_eof_in_op", &net.OpError{Op: "read", Err: io.ErrUnexpectedEOF}, ClassificationTransient},
		{"joined_canceled_with_timeout", errors.Join(context.Canceled, &timeoutOnlyError{}), ClassificationPermanent},
		{"joined_canceled_with_econnreset", errors.Join(context.Canceled, syscall.ECONNRESET), ClassificationPermanent},
		{"random", errors.New("ouch"), ClassificationPermanent},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := classifyError(c.err)
			assert.Equal(t, c.want, got, "err=%v", c.err)
		})
	}
}

func TestIsContextErr(t *testing.T) {
	t.Run("nil", func(t *testing.T) {
		assert.False(t, isContextErr(nil))
	})
	t.Run("canceled", func(t *testing.T) {
		assert.True(t, isContextErr(context.Canceled))
	})
	t.Run("deadline", func(t *testing.T) {
		assert.True(t, isContextErr(context.DeadlineExceeded))
	})
	t.Run("joined_canceled_with_econnreset", func(t *testing.T) {
		assert.True(t, isContextErr(errors.Join(context.Canceled, syscall.ECONNRESET)))
	})
	t.Run("random", func(t *testing.T) {
		assert.False(t, isContextErr(errors.New("nope")))
	})
	t.Run("oprrror_wrapping_deadline", func(t *testing.T) {
		err := &net.OpError{Op: "read", Err: context.DeadlineExceeded}
		assert.True(t, isContextErr(err))
	})
}
