package agentrouter_test

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"connectrpc.com/connect"

	agentrouter "github.com/tetrateio/agentrouter-go"

	catalogv1 "github.com/tetrateio/agentrouter-go/genapi/api/tars/catalog/v1"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/catalog/v1/catalogv1connect"
)

// fakeCatalog implements the CatalogServiceHandler with stubs that record
// the inbound Authorization / User-Agent headers and return canned data.
type fakeCatalog struct {
	catalogv1connect.UnimplementedCatalogServiceHandler

	lastAuth      string
	lastAPIKey    string
	lastUA        string
	listCalls     int
	failListUntil int
}

func (f *fakeCatalog) ListProviders(_ context.Context, req *connect.Request[catalogv1.ListProvidersRequest]) (*connect.Response[catalogv1.ListProvidersResponse], error) {
	f.listCalls++
	f.lastAuth = req.Header().Get("Authorization")
	f.lastAPIKey = req.Header().Get("X-Api-Key")
	f.lastUA = req.Header().Get("User-Agent")
	if f.listCalls <= f.failListUntil {
		return nil, connect.NewError(connect.CodeUnavailable, errors.New("flaky"))
	}
	return connect.NewResponse(&catalogv1.ListProvidersResponse{
		Providers: []*catalogv1.Provider{{Id: "openai", Name: "openai", DisplayName: "OpenAI"}},
	}), nil
}

func newCatalogServer(t *testing.T, fake *fakeCatalog) string {
	t.Helper()
	mux := http.NewServeMux()
	path, h := catalogv1connect.NewCatalogServiceHandler(fake)
	mux.Handle(path, h)
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return srv.URL
}

func TestNew_BaseURLRoundTrip(t *testing.T) {
	fake := &fakeCatalog{}
	url := newCatalogServer(t, fake)
	c, err := agentrouter.New(context.Background(), agentrouter.WithBaseURL(url))
	if err != nil {
		t.Fatal(err)
	}
	resp, err := c.Catalog().ListProviders(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if got := len(resp.GetProviders()); got != 1 {
		t.Errorf("providers = %d, want 1", got)
	}
	if fake.lastUA == "" {
		t.Error("User-Agent not set")
	}
}

func TestNew_APIKey_AuthInjected(t *testing.T) {
	fake := &fakeCatalog{}
	url := newCatalogServer(t, fake)
	c, _ := agentrouter.New(context.Background(), agentrouter.WithBaseURL(url), agentrouter.WithAPIKey("tat_live_xyz"))
	if _, err := c.Catalog().ListProviders(context.Background()); err != nil {
		t.Fatal(err)
	}
	if fake.lastAuth != "tat_live_xyz" {
		t.Errorf("Authorization = %q, want %q", fake.lastAuth, "tat_live_xyz")
	}
	if fake.lastAPIKey != "tat_live_xyz" {
		t.Errorf("X-Api-Key = %q, want %q", fake.lastAPIKey, "tat_live_xyz")
	}
}

func TestNew_SessionToken_AuthInjected(t *testing.T) {
	fake := &fakeCatalog{}
	url := newCatalogServer(t, fake)
	c, _ := agentrouter.New(context.Background(), agentrouter.WithBaseURL(url), agentrouter.WithSessionToken("session.jwt"))
	if _, err := c.Catalog().ListProviders(context.Background()); err != nil {
		t.Fatal(err)
	}
	if fake.lastAuth != "Bearer session.jwt" {
		t.Errorf("Authorization = %q, want %q", fake.lastAuth, "Bearer session.jwt")
	}
	if fake.lastAPIKey != "" {
		t.Errorf("X-Api-Key should be empty for session-token auth, got %q", fake.lastAPIKey)
	}
}

func TestNew_BothCredentials_Rejected(t *testing.T) {
	_, err := agentrouter.New(context.Background(), agentrouter.WithAPIKey("k"), agentrouter.WithSessionToken("t"))
	if err == nil {
		t.Fatal("expected error for both credentials")
	}
}

func TestNew_TypedError(t *testing.T) {
	denying := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/connect+json")
		w.WriteHeader(401)
		_, _ = w.Write([]byte(`{"code":"unauthenticated","message":"not allowed"}`))
	})
	srv := httptest.NewServer(denying)
	defer srv.Close()
	c, _ := agentrouter.New(context.Background(), agentrouter.WithBaseURL(srv.URL))
	_, err := c.Catalog().ListProviders(context.Background())
	if err == nil {
		t.Fatal("expected error")
	}
	var fe *agentrouter.Error
	if !errors.As(err, &fe) {
		t.Fatalf("expected *agentrouter.Error, got %T", err)
	}
	if fe.Code != agentrouter.CodeUnauthenticated {
		t.Errorf("Code = %v, want Unauthenticated", fe.Code)
	}
}

func TestNew_RetriesUnavailable(t *testing.T) {
	fake := &fakeCatalog{failListUntil: 2}
	url := newCatalogServer(t, fake)
	c, _ := agentrouter.New(context.Background(), agentrouter.WithBaseURL(url),
		agentrouter.WithRetryPolicy(agentrouter.RetryPolicy{MaxAttempts: 3, InitialBackoff: 10 * time.Millisecond, MaxBackoff: 50 * time.Millisecond}))
	if _, err := c.Catalog().ListProviders(context.Background()); err != nil {
		t.Errorf("expected eventual success, got %v", err)
	}
	if fake.listCalls != 3 {
		t.Errorf("calls = %d, want 3", fake.listCalls)
	}
}

func TestEndpoints_OverrideBaseURL(t *testing.T) {
	// When WithBaseURL and WithEndpoints both set the catalog URL, the
	// per-service URL wins.
	fake := &fakeCatalog{}
	url := newCatalogServer(t, fake)
	c, err := agentrouter.New(context.Background(),
		agentrouter.WithBaseURL("http://127.0.0.1:1"), // dead
		agentrouter.WithEndpoints(agentrouter.Endpoints{Catalog: url}),
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := c.Catalog().ListProviders(context.Background()); err != nil {
		t.Errorf("expected per-service URL to win, got %v", err)
	}
}

func TestClient_AllAccessorsNonNil(t *testing.T) {
	c, err := agentrouter.New(context.Background(), agentrouter.WithBaseURL("http://127.0.0.1:1"))
	if err != nil {
		t.Fatal(err)
	}
	// Every service accessor must return a non-nil handle so callers can
	// chain into the unary methods without a nil-deref.
	if c.Me() == nil || c.Identity() == nil || c.Customers() == nil ||
		c.Tenancy() == nil || c.Projects() == nil || c.Catalog() == nil ||
		c.AIKeys() == nil || c.Inference() == nil || c.Clients() == nil ||
		c.Telemetry() == nil || c.MCP() == nil {
		t.Fatal("a service accessor returned nil")
	}
	if c.MCP().Profiles() == nil || c.MCP().Catalog() == nil {
		t.Fatal("MCP sub-accessor returned nil")
	}
}
