package client

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"security-center-mcp/internal/config"
)

const testAPIKey = "sc_test_key_0123456789"

// newTestClient builds a Client pointed at srv.
func newTestClient(srv *httptest.Server) *Client {
	return New(config.Config{BaseURL: srv.URL, APIKey: testAPIKey})
}

// writeEnvelope writes a backend-style response envelope with HTTP 200.
func writeEnvelope(w http.ResponseWriter, code int, message string, data any) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"code":      code,
		"message":   message,
		"data":      data,
		"timestamp": time.Now().Unix(),
	})
}

func TestNew(t *testing.T) {
	c := New(config.Config{BaseURL: "http://example.com/", APIKey: "k"})
	if c.BaseURL != "http://example.com" {
		t.Errorf("BaseURL = %q, want trailing slash trimmed", c.BaseURL)
	}
	if c.APIKey != "k" {
		t.Errorf("APIKey = %q, want %q", c.APIKey, "k")
	}
	if c.HTTP == nil {
		t.Fatal("HTTP client is nil")
	}
	if c.HTTP.Timeout != DefaultTimeout {
		t.Errorf("HTTP timeout = %v, want %v", c.HTTP.Timeout, DefaultTimeout)
	}
}

func TestGetSuccessUnwrapsData(t *testing.T) {
	type tag struct {
		Key   string `json:"key"`
		Value string `json:"value"`
	}
	type asset struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
		Tags []tag  `json:"tags"`
	}
	want := asset{
		ID:   42,
		Name: "web-01",
		Tags: []tag{{Key: "env", Value: "prod"}, {Key: "team", Value: "sec"}},
	}

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/caasm/assets/42" {
			t.Errorf("path = %q, want /api/v1/caasm/assets/42", r.URL.Path)
		}
		writeEnvelope(w, 0, "success", want)
	}))
	defer srv.Close()

	var got asset
	if err := newTestClient(srv).Get(context.Background(), "/api/v1/caasm/assets/42", nil, &got); err != nil {
		t.Fatalf("Get: %v", err)
	}
	if got.ID != want.ID || got.Name != want.Name {
		t.Errorf("got %+v, want %+v", got, want)
	}
	if len(got.Tags) != 2 || got.Tags[0] != want.Tags[0] || got.Tags[1] != want.Tags[1] {
		t.Errorf("nested tags = %+v, want %+v", got.Tags, want.Tags)
	}
}

func TestGetEnvelopeErrorCodes(t *testing.T) {
	tests := []struct {
		name       string
		code       int
		retryAfter string // Retry-After header value, "" = absent
		wantRetry  time.Duration
		check      func(*APIError) bool
	}{
		{"unauthorized", http.StatusUnauthorized, "", 0, (*APIError).IsUnauthorized},
		{"forbidden", http.StatusForbidden, "", 0, (*APIError).IsForbidden},
		{"not found", http.StatusNotFound, "", 0, (*APIError).IsNotFound},
		{"rate limited", http.StatusTooManyRequests, "60", 60 * time.Second, (*APIError).IsRateLimited},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if tt.retryAfter != "" {
					w.Header().Set("Retry-After", tt.retryAfter)
				}
				writeEnvelope(w, tt.code, "boom", nil)
			}))
			defer srv.Close()

			err := newTestClient(srv).Get(context.Background(), "/x", nil, nil)
			if err == nil {
				t.Fatal("expected error, got nil")
			}
			var apiErr *APIError
			if !errors.As(err, &apiErr) {
				t.Fatalf("error type = %T, want *APIError", err)
			}
			if apiErr.Code != tt.code {
				t.Errorf("Code = %d, want %d", apiErr.Code, tt.code)
			}
			if apiErr.Message != "boom" {
				t.Errorf("Message = %q, want %q", apiErr.Message, "boom")
			}
			if apiErr.HTTPStatus != http.StatusOK {
				t.Errorf("HTTPStatus = %d, want 200 (envelope error)", apiErr.HTTPStatus)
			}
			if apiErr.RetryAfter != tt.wantRetry {
				t.Errorf("RetryAfter = %v, want %v", apiErr.RetryAfter, tt.wantRetry)
			}
			if !tt.check(apiErr) {
				t.Errorf("helper for code %d returned false", tt.code)
			}
		})
	}
}

func TestGetHTTPErrorWithoutEnvelope(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "internal boom", http.StatusInternalServerError)
	}))
	defer srv.Close()

	err := newTestClient(srv).Get(context.Background(), "/x", nil, nil)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	var apiErr *APIError
	if errors.As(err, &apiErr) {
		t.Fatalf("transport-level error must not be *APIError, got %v", apiErr)
	}
}

func TestGetInvalidEnvelope(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("not json"))
	}))
	defer srv.Close()

	if err := newTestClient(srv).Get(context.Background(), "/x", nil, nil); err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestGetNullDataLeavesOutUntouched(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writeEnvelope(w, 0, "success", nil)
	}))
	defer srv.Close()

	out := map[string]string{"keep": "me"}
	if err := newTestClient(srv).Get(context.Background(), "/x", nil, &out); err != nil {
		t.Fatalf("Get: %v", err)
	}
	if len(out) != 1 || out["keep"] != "me" {
		t.Errorf("out was modified: %v", out)
	}
}

func TestGetSendsHeadersAndQuery(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got := r.Header.Get("X-API-Key"); got != testAPIKey {
			t.Errorf("X-API-Key = %q, want %q", got, testAPIKey)
		}
		if got := r.Header.Get("Accept"); got != "application/json" {
			t.Errorf("Accept = %q, want application/json", got)
		}
		if got := r.Header.Get("User-Agent"); got != userAgent {
			t.Errorf("User-Agent = %q, want %q", got, userAgent)
		}
		q := r.URL.Query()
		if got := q.Get("page"); got != "2" {
			t.Errorf("query page = %q, want 2", got)
		}
		if got := q.Get("q"); got != "foo bar" {
			t.Errorf("query q = %q, want %q", got, "foo bar")
		}
		writeEnvelope(w, 0, "success", nil)
	}))
	defer srv.Close()

	query := url.Values{"page": {"2"}, "q": {"foo bar"}}
	if err := newTestClient(srv).Get(context.Background(), "/api/v1/caasm/assets", query, nil); err != nil {
		t.Fatalf("Get: %v", err)
	}
}

func TestDoSendsMethodQueryAndBody(t *testing.T) {
	type payload struct {
		Title    string  `json:"title"`
		Severity string  `json:"severity"`
		CVSS     float64 `json:"cvss_score"`
	}
	want := payload{Title: "xss in /search", Severity: "high", CVSS: 7.5}

	for _, method := range []string{http.MethodPost, http.MethodPut} {
		t.Run(method, func(t *testing.T) {
			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Method != method {
					t.Errorf("method = %q, want %q", r.Method, method)
				}
				if r.URL.Path != "/api/v1/vulnerabilities" {
					t.Errorf("path = %q, want /api/v1/vulnerabilities", r.URL.Path)
				}
				if got := r.URL.Query().Get("source"); got != "mcp" {
					t.Errorf("query source = %q, want mcp", got)
				}
				if got := r.Header.Get("Content-Type"); got != "application/json" {
					t.Errorf("Content-Type = %q, want application/json", got)
				}
				if got := r.Header.Get("X-API-Key"); got != testAPIKey {
					t.Errorf("X-API-Key = %q, want %q", got, testAPIKey)
				}
				if got := r.Header.Get("User-Agent"); got != userAgent {
					t.Errorf("User-Agent = %q, want %q", got, userAgent)
				}
				var got payload
				if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
					t.Errorf("decode request body: %v", err)
				}
				if got != want {
					t.Errorf("body = %+v, want %+v", got, want)
				}
				writeEnvelope(w, 0, "success", map[string]string{"public_id": "VULN-0001"})
			}))
			defer srv.Close()

			var out struct {
				PublicID string `json:"public_id"`
			}
			query := url.Values{"source": {"mcp"}}
			err := newTestClient(srv).Do(context.Background(), method, "/api/v1/vulnerabilities", query, want, &out)
			if err != nil {
				t.Fatalf("Do: %v", err)
			}
			if out.PublicID != "VULN-0001" {
				t.Errorf("out.PublicID = %q, want VULN-0001", out.PublicID)
			}
		})
	}
}

func TestDoNilBodySendsNoBody(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got := r.Header.Get("Content-Type"); got != "" {
			t.Errorf("Content-Type = %q, want empty for nil body", got)
		}
		body, _ := io.ReadAll(r.Body)
		if len(body) != 0 {
			t.Errorf("request body = %q, want empty", body)
		}
		writeEnvelope(w, 0, "success", nil)
	}))
	defer srv.Close()

	if err := newTestClient(srv).Do(context.Background(), http.MethodPost, "/x", nil, nil, nil); err != nil {
		t.Fatalf("Do: %v", err)
	}
}

func TestDoEnvelopeErrorMatchesGetMapping(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Retry-After", "30")
		writeEnvelope(w, http.StatusTooManyRequests, "quota exhausted", nil)
	}))
	defer srv.Close()

	err := newTestClient(srv).Do(context.Background(), http.MethodPost, "/x", nil, map[string]string{"a": "b"}, nil)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	var apiErr *APIError
	if !errors.As(err, &apiErr) {
		t.Fatalf("error type = %T, want *APIError", err)
	}
	if apiErr.Code != http.StatusTooManyRequests || !apiErr.IsRateLimited() {
		t.Errorf("Code = %d, want 429", apiErr.Code)
	}
	if apiErr.Message != "quota exhausted" {
		t.Errorf("Message = %q, want %q", apiErr.Message, "quota exhausted")
	}
	if apiErr.HTTPStatus != http.StatusOK {
		t.Errorf("HTTPStatus = %d, want 200 (envelope error)", apiErr.HTTPStatus)
	}
	if apiErr.RetryAfter != 30*time.Second {
		t.Errorf("RetryAfter = %v, want 30s", apiErr.RetryAfter)
	}
}

func TestDoHTTPErrorWithoutEnvelope(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "bad gateway", http.StatusBadGateway)
	}))
	defer srv.Close()

	err := newTestClient(srv).Do(context.Background(), http.MethodPut, "/x/1", nil, map[string]string{"a": "b"}, nil)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	var apiErr *APIError
	if errors.As(err, &apiErr) {
		t.Fatalf("transport-level error must not be *APIError, got %v", apiErr)
	}
	// The error names method and path so the failed request is identifiable.
	if !strings.Contains(err.Error(), "PUT /x/1") {
		t.Errorf("error %q does not name the request method and path", err)
	}
}

func TestPutRawSendsRawBytesAndContentType(t *testing.T) {
	// Binary payload with bytes JSON would mangle — must arrive verbatim.
	payload := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0xff, 0xfe}

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPut {
			t.Errorf("method = %q, want PUT", r.Method)
		}
		if got := r.Header.Get("Content-Type"); got != "image/png" {
			t.Errorf("Content-Type = %q, want image/png (the presigned value)", got)
		}
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Errorf("read request body: %v", err)
		}
		if !bytes.Equal(body, payload) {
			t.Errorf("body = %v, want byte-identical %v", body, payload)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer srv.Close()

	rawURL := srv.URL + "/bucket/attachments/1.png?X-Amz-Signature=abc123"
	if err := newTestClient(srv).PutRaw(context.Background(), rawURL, payload, "image/png"); err != nil {
		t.Fatalf("PutRaw: %v", err)
	}
}

func TestPutRawDoesNotSendAPIKey(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Security assertion: the API key must never leak to the object
		// store — the presigned URL itself is the credential.
		for name := range r.Header {
			if strings.Contains(strings.ToLower(name), "api-key") {
				t.Errorf("credential header %q sent to presigned URL", name)
			}
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer srv.Close()

	rawURL := srv.URL + "/bucket/k.png?X-Amz-Signature=abc123"
	if err := newTestClient(srv).PutRaw(context.Background(), rawURL, []byte("x"), "image/png"); err != nil {
		t.Fatalf("PutRaw: %v", err)
	}
}

func TestPutRawHTTPError(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "AccessDenied", http.StatusForbidden)
	}))
	defer srv.Close()

	rawURL := srv.URL + "/bucket/k.png?X-Amz-Signature=SECRETSIG&X-Amz-Credential=AKIASECRET"
	err := newTestClient(srv).PutRaw(context.Background(), rawURL, []byte("x"), "image/png")
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !strings.Contains(err.Error(), "403") {
		t.Errorf("error %q does not name the HTTP status", err)
	}
	if !strings.Contains(err.Error(), strings.TrimPrefix(srv.URL, "http://")) {
		t.Errorf("error %q does not name the URL host", err)
	}
	// The signed query must never be quoted — it is a credential.
	if strings.Contains(err.Error(), "SECRETSIG") || strings.Contains(err.Error(), "AKIASECRET") {
		t.Errorf("error %q leaks the presigned query credentials", err)
	}
}
