package acceptance

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"
)

type Client struct {
	baseURL    string
	httpClient *http.Client
	token      string
}

type Envelope struct {
	Code uint32          `json:"code"`
	Msg  string          `json:"msg"`
	Data json.RawMessage `json:"data"`
}

func newClient(c Config) *Client {
	return &Client{
		baseURL: strings.TrimRight(c.StagingURL, "/"),
		httpClient: &http.Client{
			Timeout: c.HTTPTimeout,
		},
	}
}

func (c *Client) WithToken(token string) *Client {
	next := *c
	next.token = token
	return &next
}

func (c *Client) Get(ctx context.Context, path string, query url.Values) (*Envelope, []byte, int, error) {
	return c.Do(ctx, http.MethodGet, path, query, nil)
}

func (c *Client) PostJSON(ctx context.Context, path string, payload any) (*Envelope, []byte, int, error) {
	return c.Do(ctx, http.MethodPost, path, nil, payload)
}

func (c *Client) Do(ctx context.Context, method string, path string, query url.Values, payload any) (*Envelope, []byte, int, error) {
	target, err := url.Parse(c.baseURL + "/" + strings.TrimLeft(path, "/"))
	if err != nil {
		return nil, nil, 0, fmt.Errorf("build request URL: %w", err)
	}
	if query != nil {
		target.RawQuery = query.Encode()
	}

	var body io.Reader
	if payload != nil {
		encoded, err := json.Marshal(payload)
		if err != nil {
			return nil, nil, 0, fmt.Errorf("marshal request body: %w", err)
		}
		body = bytes.NewReader(encoded)
	}

	req, err := http.NewRequestWithContext(ctx, method, target.String(), body)
	if err != nil {
		return nil, nil, 0, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("User-Agent", "hifast-acceptance/1.0")
	req.Header.Set("Login-Type", "email")
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if c.token != "" {
		req.Header.Set("Authorization", c.token)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, nil, 0, fmt.Errorf("%s %s: %w", method, target.Redacted(), err)
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	raw, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
	if err != nil {
		return nil, nil, resp.StatusCode, fmt.Errorf("read response: %w", err)
	}

	envelope := &Envelope{}
	if strings.Contains(resp.Header.Get("Content-Type"), "application/json") && len(raw) > 0 {
		if err := json.Unmarshal(raw, envelope); err != nil {
			return nil, raw, resp.StatusCode, fmt.Errorf("decode response envelope: %w", err)
		}
	}
	return envelope, raw, resp.StatusCode, nil
}

func assertOK(t *testing.T, envelope *Envelope, raw []byte, status int) {
	t.Helper()
	if status != http.StatusOK {
		t.Fatalf("http status = %d, want 200; body=%s", status, sanitizeBody(raw))
	}
	if envelope == nil || envelope.Code != 200 {
		t.Fatalf("api code = %d, want 200; msg=%q body=%s", envelope.Code, envelope.Msg, sanitizeBody(raw))
	}
}

func assertAPIError(t *testing.T, envelope *Envelope, raw []byte, status int) {
	t.Helper()
	if status != http.StatusOK {
		t.Fatalf("http status = %d, want 200 API envelope; body=%s", status, sanitizeBody(raw))
	}
	if envelope == nil || envelope.Code == 200 {
		t.Fatalf("api code = %d, want non-200; body=%s", envelope.Code, sanitizeBody(raw))
	}
}

func sanitizeBody(raw []byte) string {
	value := string(raw)
	if len(value) > 512 {
		value = value[:512] + "...<truncated>"
	}
	return value
}
