package checker

import (
	"context"
	"errors"
	"syscall"
	"testing"

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

func TestHintFor(t *testing.T) {
	t.Run("nil_err_returns_empty", func(t *testing.T) {
		assert.Equal(t, "", hintFor("tcp", nil))
		assert.Equal(t, "", hintFor("anything", nil))
	})

	t.Run("context_canceled_uniform", func(t *testing.T) {
		// Cancellation is always reported as «Проверка отменена.» across
		// all testIDs.
		for _, id := range []string{"tcp", "greet", "auth", "connect", "udp", "voice-quality", "api", "unknown"} {
			assert.Equal(t, "Проверка отменена.", hintFor(id, context.Canceled), "id=%s", id)
			assert.Equal(t, "Проверка отменена.", hintFor(id, context.DeadlineExceeded), "id=%s", id)
		}
	})

	cases := []struct {
		name      string
		testID    string
		err       error
		substring string
	}{
		{"tcp_timeout", "tcp", &timeoutOnlyError{}, "таймаут"},
		{"greet_bad_version_mentions_socks5", "greet", ErrSocks5BadVersion, "SOCKS5"},
		{"greet_bad_version_mentions_negation", "greet", ErrSocks5BadVersion, "не"},
		{"greet_rejected_all_auth_mentions_auth_or_methods", "greet", ErrSocks5RejectedAllAuth, "авторизаци"},
		{"auth_login", "auth", ErrAuthRejected, "Логин"},
		{"auth_password", "auth", ErrAuthRejected, "паро"},
		{"connect_refused_rep05", "connect", ErrSocks5Reply{Code: 0x05}, "REP=05"},
		{"connect_refused_text", "connect", ErrSocks5Reply{Code: 0x05}, "connection refused"},
		{"connect_unsupported_rep07", "connect", ErrSocks5Reply{Code: 0x07}, "REP=07"},
		{"udp_unsupported_mentions_udp", "udp", ErrSocks5Reply{Code: 0x07}, "UDP"},
		{"udp_unsupported_mentions_unsupported", "udp", ErrSocks5Reply{Code: 0x07}, "не поддерж"},
		{"udp_atyp_ipv6", "udp", ErrUnsupportedRelayATYP, "IPv6"},
		{"voice_quality_no_mapped_xor", "voice-quality", ErrSTUNNoMappedAddress, "XOR-MAPPED"},
		{"voice_quality_timeout_mentions_stun", "voice-quality", &timeoutOnlyError{}, "STUN"},
		{"api_timeout_mentions_api_or_timeout", "api", &timeoutOnlyError{}, "таймаут"},
		{"unknown_test_fallback_id", "unknown_test", errors.New("oops"), "unknown_test"},
		{"unknown_test_fallback_err", "unknown_test", errors.New("oops"), "oops"},
		{"tcp_fallback_friendly_name", "tcp", errors.New("weird"), "TCP"},
	}

	for _, c := range cases {
		c := c
		t.Run(c.name, func(t *testing.T) {
			got := hintFor(c.testID, c.err)
			assert.Contains(t, got, c.substring, "got=%q", got)
		})
	}
}

func TestHintFor_AllSocks5ReplyCodesCovered(t *testing.T) {
	// Every documented REP code (0x01..0x08) should produce a non-empty
	// hint when surfaced via "connect" or "udp".
	for code := byte(0x01); code <= 0x08; code++ {
		err := ErrSocks5Reply{Code: code}
		assert.NotEmpty(t, hintFor("connect", err), "connect code=%02X", code)
		assert.NotEmpty(t, hintFor("udp", err), "udp code=%02X", code)
	}
	// Unknown REP code (0xFE) still gets a sensible fallback rather than
	// an empty string.
	err := ErrSocks5Reply{Code: 0xFE}
	assert.NotEmpty(t, hintFor("connect", err))
	assert.NotEmpty(t, hintFor("udp", err))
}

func TestHintFor_PerStepBranches(t *testing.T) {
	cases := []struct {
		name      string
		testID    string
		err       error
		substring string
	}{
		// tcp: ECONNREFUSED + generic fallback
		{"tcp_econnrefused", "tcp", syscall.ECONNREFUSED, "отклонил"},
		{"tcp_generic", "tcp", errors.New("dial fail"), "TCP"},

		// greet: short reply, timeout, fallback
		{"greet_short_reply", "greet", ErrShortReply, "укороченный"},
		{"greet_timeout", "greet", &timeoutOnlyError{}, "вовремя"},
		{"greet_fallback", "greet", errors.New("weird"), "приветствие"},

		// auth: credential too long, short reply, timeout, fallback
		{"auth_cred_too_long", "auth", ErrCredentialTooLong, "255"},
		{"auth_short_reply", "auth", ErrShortReply, "укороченный"},
		{"auth_timeout", "auth", &timeoutOnlyError{}, "вовремя"},
		{"auth_fallback", "auth", errors.New("weird"), "авторизация"},

		// connect: host too long, short reply, timeout, generic REP, fallback
		{"connect_host_too_long", "connect", ErrHostTooLong, "255"},
		{"connect_short_reply", "connect", ErrShortReply, "укороченный"},
		{"connect_timeout", "connect", &timeoutOnlyError{}, "вовремя"},
		{"connect_generic_rep", "connect", ErrSocks5Reply{Code: 0x03}, "REP=03"},
		{"connect_unknown_rep", "connect", ErrSocks5Reply{Code: 0xFE}, "REP=FE"},
		{"connect_fallback", "connect", errors.New("weird"), "TCP-туннель"},

		// udp: short reply, timeout, fallback, non-7 REP
		{"udp_short_reply", "udp", ErrShortReply, "укороченный"},
		{"udp_timeout", "udp", &timeoutOnlyError{}, "вовремя"},
		{"udp_other_rep", "udp", ErrSocks5Reply{Code: 0x05}, "REP=05"},
		{"udp_unknown_rep", "udp", ErrSocks5Reply{Code: 0xFE}, "REP=FE"},
		{"udp_fallback", "udp", errors.New("weird"), "UDP ASSOCIATE"},

		// voice-quality: every sentinel branch (collapsed in 2026-05-01
		// rewrite into a single user-visible message rather than
		// per-error "магник cookie" / "семейство адресов" exposition)
		{"voice_quality_too_short", "voice-quality", ErrSTUNTooShort, "мусор"},
		{"voice_quality_bad_magic", "voice-quality", ErrSTUNBadMagicCookie, "мусор"},
		{"voice_quality_not_success", "voice-quality", ErrSTUNNotSuccess, "мусор"},
		{"voice_quality_txid_mismatch", "voice-quality", ErrSTUNTxIDMismatch, "мусор"},
		{"voice_quality_unsupported_family", "voice-quality", ErrSTUNUnsupportedFamily, "мусор"},
		{"voice_quality_fallback", "voice-quality", errors.New("weird"), "качество"},

		// api: timeout vs generic
		{"api_timeout", "api", &timeoutOnlyError{}, "таймаут"},
		{"api_generic", "api", errors.New("tls boom"), "TLS"},

		// socks5ReplyHint via uncategorised step (default branch)
		// — we can't reach it via hintFor with current testIDs, but the
		//   default formatter still needs to be exercised.
	}
	for _, c := range cases {
		c := c
		t.Run(c.name, func(t *testing.T) {
			got := hintFor(c.testID, c.err)
			assert.Contains(t, got, c.substring, "got=%q", got)
		})
	}
}

func TestSocks5ReplyHint_DefaultStep(t *testing.T) {
	// socks5ReplyHint("", code) hits the final fallback formatter.
	got := socks5ReplyHint("", 0x03)
	assert.Contains(t, got, "REP=03")
	got = socks5ReplyHint("", 0xFE)
	assert.Contains(t, got, "REP=FE")
}

func TestTcpFriendlyName(t *testing.T) {
	cases := map[string]string{
		"tcp":           "TCP",
		"greet":         "приветствие SOCKS5",
		"auth":          "авторизация SOCKS5",
		"connect":       "TCP-туннель к Discord",
		"udp":           "UDP ASSOCIATE",
		"voice-quality": "качество UDP-канала",
		"api":           "Discord API",
		"weirdo":        "weirdo",
	}
	for in, want := range cases {
		t.Run(in, func(t *testing.T) {
			assert.Equal(t, want, tcpFriendlyName(in))
		})
	}
}
