package controld

import (
	"context"
	"errors"
	"net"
	"net/http"
	"net/http/httptest"
	"strings"
	"syscall"
	"testing"
)

// errRoundTripper fails the hostname attempt and the direct-ip attempt with
// different errors, mimicking the Firewall Mode incident: the hostname attempt is
// denied by a local firewall (WSAEACCES on Windows) while the direct-ip fallback
// reports an unreachable IPv6 route.
type errRoundTripper struct {
	hostname string
	firstErr error
	fbErr    error
	fbCalled bool
}

func (rt *errRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.URL.Host == rt.hostname {
		return nil, &net.OpError{Op: "dial", Net: "tcp4", Err: rt.firstErr}
	}
	rt.fbCalled = true
	if rt.fbErr == nil {
		return &http.Response{
			StatusCode: http.StatusOK,
			Body:       http.NoBody,
			Request:    req,
		}, nil
	}
	return nil, &net.OpError{Op: "dial", Net: "tcp6", Err: rt.fbErr}
}

// wsaEACCES is WSAEACCES (10013): "An attempt was made to access a socket in a way
// forbidden by its access permissions." The value is what Windows reports when a
// WFP filter denies the connect; it is used here as a plain errno so the test runs
// on every platform.
const wsaEACCES = syscall.Errno(10013)

func TestDoWithFallbackPreservesFirstError(t *testing.T) {
	const (
		hostname = "api.controld.com"
		apiIP    = "147.185.34.1"
	)
	rt := &errRoundTripper{
		hostname: hostname,
		firstErr: wsaEACCES,
		fbErr:    syscall.EHOSTUNREACH,
	}
	req, err := http.NewRequest(http.MethodPost, "https://"+hostname+"/utility", nil)
	if err != nil {
		t.Fatal(err)
	}

	resp, err := doWithFallback(context.Background(), &http.Client{Transport: rt}, req, apiIP)
	if err == nil {
		t.Fatalf("expected an error, got response %v", resp)
	}
	if !rt.fbCalled {
		t.Error("direct-ip fallback was not attempted")
	}

	// The actionable failure must survive: an operator reading this error has to be
	// able to tell "the host is blocking us" from "the network is down".
	if !errors.Is(err, wsaEACCES) {
		t.Errorf("first-attempt error (WSAEACCES) was lost, got: %v", err)
	}
	if !errors.Is(err, syscall.EHOSTUNREACH) {
		t.Errorf("fallback error was lost, got: %v", err)
	}
	if got := err.Error(); !strings.Contains(got, apiIP) {
		t.Errorf("error does not mention the fallback ip %q: %v", apiIP, got)
	}
}

func TestDoWithFallbackSucceedsOnFallback(t *testing.T) {
	const hostname = "api.controld.com"
	rt := &errRoundTripper{hostname: hostname, firstErr: wsaEACCES}
	req, err := http.NewRequest(http.MethodPost, "https://"+hostname+"/utility", nil)
	if err != nil {
		t.Fatal(err)
	}

	resp, err := doWithFallback(context.Background(), &http.Client{Transport: rt}, req, "147.185.34.1")
	if err != nil {
		t.Fatalf("expected the fallback to succeed, got: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("StatusCode = %d, want %d", resp.StatusCode, http.StatusOK)
	}
}

func TestDoWithFallbackNoFallbackOnSuccess(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer srv.Close()

	req, err := http.NewRequest(http.MethodPost, srv.URL, nil)
	if err != nil {
		t.Fatal(err)
	}
	resp, err := doWithFallback(context.Background(), srv.Client(), req, "127.0.0.2")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("StatusCode = %d, want %d", resp.StatusCode, http.StatusOK)
	}
}

// TestDoWithFallbackComposesHostnameAttemptFirst pins the order of the composed error.
//
// The order is not cosmetic. cmd/cli's preflight retry predicate classifies this error
// with errors.As, which returns the first match in the tree, so whichever attempt is
// wrapped first decides whether processCDFlags keeps backing off or fails fast. That
// predicate lives in another package and cannot be called from here, so this test
// guards the property it depends on: the hostname attempt - the one that carries the
// diagnosis - must come first.
func TestDoWithFallbackComposesHostnameAttemptFirst(t *testing.T) {
	const hostname = "api.controld.com"
	rt := &errRoundTripper{
		hostname: hostname,
		firstErr: wsaEACCES,
		fbErr:    syscall.EHOSTUNREACH,
	}
	req, err := http.NewRequest(http.MethodPost, "https://"+hostname+"/utility", nil)
	if err != nil {
		t.Fatal(err)
	}

	_, gotErr := doWithFallback(context.Background(), &http.Client{Transport: rt}, req, "147.185.34.1")
	if gotErr == nil {
		t.Fatal("expected both attempts to fail")
	}

	var opErr *net.OpError
	if !errors.As(gotErr, &opErr) {
		t.Fatalf("no net.OpError in the chain: %v", gotErr)
	}
	if !errors.Is(opErr.Err, wsaEACCES) {
		t.Errorf("first OpError in the chain is %v, want the hostname attempt (%v)", opErr.Err, wsaEACCES)
	}
	// The tcp4/tcp6 split distinguishes the two attempts in the fake transport.
	if opErr.Net != "tcp4" {
		t.Errorf("first OpError is from the %s attempt, want tcp4 (hostname)", opErr.Net)
	}
}
