package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestNormalizePath(t *testing.T) {
	cases := map[string]string{
		"/campaigns":    "/v1/campaigns",
		"campaigns":     "/v1/campaigns",
		"/v1/campaigns": "/v1/campaigns",
		"/v2/campaigns": "/v2/campaigns",
		// "/verify" starts with /v but is not a version, so it must be
		// prefixed rather than treated as v-something.
		"/verify": "/v1/verify",
	}
	for in, want := range cases {
		if got := NormalizePath(in); got != want {
			t.Errorf("NormalizePath(%q) = %q, want %q", in, got, want)
		}
	}
}

func TestErrorEnvelopeSurvives(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusForbidden)
		fmt.Fprint(w, `{"error":"forbidden","message":"missing scope","code":"insufficient_scope","request_id":"req_123"}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "wmbly_x", "test")
	_, err := c.Do(context.Background(), Request{Method: http.MethodGet, Path: "/campaigns"})
	var apiErr *Error
	if !errors.As(err, &apiErr) {
		t.Fatalf("got %T, want *api.Error", err)
	}
	if apiErr.Code != "insufficient_scope" || apiErr.RequestID != "req_123" {
		t.Errorf("the machine-readable fields were lost: %+v", apiErr)
	}
	if !strings.Contains(apiErr.Error(), "req_123") {
		t.Errorf("the request id must reach the message: %s", apiErr.Error())
	}
	if StatusOf(err) != http.StatusForbidden {
		t.Errorf("StatusOf = %d", StatusOf(err))
	}
}

func TestAnonymousRequestSendsNoBearer(t *testing.T) {
	var sawAuth string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		sawAuth = r.Header.Get("Authorization")
		fmt.Fprint(w, `{}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "", "test")
	if _, err := c.Do(context.Background(), Request{Method: http.MethodPost, Path: "/auth/cli/code", Anonymous: true}); err != nil {
		t.Fatalf("anonymous request failed: %v", err)
	}
	if sawAuth != "" {
		t.Errorf("an anonymous request carried %q", sawAuth)
	}

	// A normal request with no token must fail before it reaches the network.
	if _, err := c.Do(context.Background(), Request{Method: http.MethodGet, Path: "/me"}); err == nil {
		t.Error("a request with no token should not be attempted")
	}
}

func TestPaginateFollowsTheCursor(t *testing.T) {
	pages := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cursor := r.URL.Query().Get("cursor")
		switch cursor {
		case "":
			pages++
			fmt.Fprint(w, `{"data":[{"id":"a"},{"id":"b"}],"pagination":{"has_more":true,"next_cursor":"c2"}}`)
		case "c2":
			pages++
			fmt.Fprint(w, `{"data":[{"id":"c"}],"pagination":{"has_more":false,"next_cursor":null}}`)
		default:
			t.Errorf("unexpected cursor %q", cursor)
		}
	}))
	defer srv.Close()

	c := New(srv.URL, "wmbly_x", "test")
	merged, err := c.Paginate(context.Background(), Request{Method: http.MethodGet, Path: "/campaigns"}, 10)
	if err != nil {
		t.Fatalf("paginate: %v", err)
	}
	if pages != 2 {
		t.Errorf("fetched %d pages, want 2", pages)
	}
	var doc struct {
		Data       []map[string]string `json:"data"`
		Pagination struct {
			HasMore bool `json:"has_more"`
		} `json:"pagination"`
	}
	if err := json.Unmarshal(merged, &doc); err != nil {
		t.Fatalf("merged payload is not JSON: %v", err)
	}
	if len(doc.Data) != 3 {
		t.Errorf("merged %d rows, want 3", len(doc.Data))
	}
	if doc.Pagination.HasMore {
		t.Error("the merged envelope must report no more pages")
	}
}

// An endpoint that does not use the cursor envelope comes back untouched
// rather than being flattened into an empty list.
func TestPaginateLeavesNonListPayloadsAlone(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, `{"count":7}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "wmbly_x", "test")
	out, err := c.Paginate(context.Background(), Request{Method: http.MethodGet, Path: "/unibox/count"}, 10)
	if err != nil {
		t.Fatalf("paginate: %v", err)
	}
	if !strings.Contains(string(out), `"count":7`) {
		t.Errorf("payload was rewritten: %s", out)
	}
}

// A long --all walk will meet the per-key minute budget. The client waits the
// Retry-After rather than returning a partial list the caller cannot
// distinguish from a complete one.
func TestPaginateWaitsOutARateLimit(t *testing.T) {
	limited := true
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if limited {
			limited = false
			w.Header().Set("Retry-After", "1")
			w.WriteHeader(http.StatusTooManyRequests)
			fmt.Fprint(w, `{"code":"rate_limit_exceeded","message":"slow down"}`)
			return
		}
		fmt.Fprint(w, `{"data":[{"id":"a"}],"pagination":{"has_more":false,"next_cursor":null}}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "wmbly_x", "test")
	merged, err := c.Paginate(context.Background(), Request{Method: http.MethodGet, Path: "/campaigns"}, 5)
	if err != nil {
		t.Fatalf("paginate should have waited and retried: %v", err)
	}
	if !strings.Contains(string(merged), `"id":"a"`) {
		t.Errorf("the retried page was lost: %s", merged)
	}
}

// A rate limit that never clears must still end, rather than looping until
// max-pages with the caller none the wiser.
func TestPaginateGivesUpOnAPermanentRateLimit(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Retry-After", "1")
		w.WriteHeader(http.StatusTooManyRequests)
		fmt.Fprint(w, `{"code":"rate_limit_exceeded"}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "wmbly_x", "test")
	if _, err := c.Paginate(context.Background(), Request{Method: http.MethodGet, Path: "/campaigns"}, 2); err == nil {
		t.Fatal("a permanent rate limit must surface as an error")
	}
}

// A walk cut short by --max-pages has to say so. A caller that cannot tell a
// truncated list from a complete one will act on a partial answer.
func TestPaginateReportsTruncation(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Always another page.
		fmt.Fprint(w, `{"data":[{"id":"x"}],"pagination":{"has_more":true,"next_cursor":"more"}}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "wmbly_x", "test")
	merged, err := c.Paginate(context.Background(), Request{Method: http.MethodGet, Path: "/campaigns"}, 2)
	if err != nil {
		t.Fatalf("paginate: %v", err)
	}
	var doc struct {
		Data       []map[string]string `json:"data"`
		Pagination struct {
			HasMore    bool    `json:"has_more"`
			NextCursor *string `json:"next_cursor"`
			Total      int     `json:"total"`
		} `json:"pagination"`
	}
	if err := json.Unmarshal(merged, &doc); err != nil {
		t.Fatalf("merged payload: %v", err)
	}
	if len(doc.Data) != 2 {
		t.Errorf("collected %d rows over 2 pages, want 2", len(doc.Data))
	}
	if !doc.Pagination.HasMore {
		t.Error("a walk stopped by max-pages must report has_more")
	}
	if doc.Pagination.NextCursor == nil || *doc.Pagination.NextCursor != "more" {
		t.Errorf("the cursor to resume from was lost: %+v", doc.Pagination.NextCursor)
	}
}

// And a walk that genuinely ran out still reports the end.
func TestPaginateReportsCompletion(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, `{"data":[{"id":"x"}],"pagination":{"has_more":false,"next_cursor":null}}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "wmbly_x", "test")
	merged, err := c.Paginate(context.Background(), Request{Method: http.MethodGet, Path: "/campaigns"}, 10)
	if err != nil {
		t.Fatalf("paginate: %v", err)
	}
	if !strings.Contains(string(merged), `"has_more":false`) || !strings.Contains(string(merged), `"next_cursor":null`) {
		t.Errorf("a completed walk must report the end: %s", merged)
	}
}
