package load

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

type scenario struct {
	name   string
	method string
	path   string
	body   []byte
	token  string
	header map[string]string
}

func TestCriticalEndpointLoadProfile(t *testing.T) {
	baseURL := strings.TrimRight(os.Getenv("BANKING_LOAD_BASE_URL"), "/")
	if baseURL == "" {
		t.Skip("set BANKING_LOAD_BASE_URL to run load quality gates")
	}
	concurrency := envInt("BANKING_LOAD_CONCURRENCY", 4)
	iterations := envInt("BANKING_LOAD_ITERATIONS", 20)
	client := &http.Client{Timeout: 10 * time.Second}
	scenarios := loadScenarios()
	if len(scenarios) == 0 {
		t.Fatal("no load scenarios configured")
	}

	var failures atomic.Int64
	var wg sync.WaitGroup
	start := time.Now()
	for worker := 0; worker < concurrency; worker++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			for i := 0; i < iterations; i++ {
				current := scenarios[(workerID+i)%len(scenarios)]
				if err := runScenario(client, baseURL, current); err != nil {
					failures.Add(1)
					t.Logf("%s failed: %v", current.name, err)
				}
			}
		}(worker)
	}
	wg.Wait()
	t.Logf("load profile completed scenarios=%d concurrency=%d iterations=%d duration=%s failures=%d", len(scenarios), concurrency, iterations, time.Since(start), failures.Load())
	if failures.Load() > 0 {
		t.Fatalf("load profile had %d failures", failures.Load())
	}
}

func loadScenarios() []scenario {
	customerToken := strings.TrimSpace(os.Getenv("BANKING_LOAD_CUSTOMER_TOKEN"))
	adminToken := strings.TrimSpace(os.Getenv("BANKING_LOAD_ADMIN_TOKEN"))
	scenarios := []scenario{{name: "health", method: http.MethodGet, path: "/healthz"}}
	if customerToken != "" {
		scenarios = append(scenarios,
			scenario{name: "account-list", method: http.MethodGet, path: "/v1/accounts", token: customerToken},
			scenario{name: "transfer-list", method: http.MethodGet, path: "/v1/transfers", token: customerToken},
			scenario{name: "card-list", method: http.MethodGet, path: "/v1/virtual-cards", token: customerToken},
		)
	}
	if adminToken != "" {
		scenarios = append(scenarios,
			scenario{name: "admin-dashboard", method: http.MethodGet, path: "/v1/admin/dashboard/summary", token: adminToken},
			scenario{name: "admin-backoffice", method: http.MethodGet, path: "/v1/admin/backoffice/dashboard", token: adminToken},
		)
	}
	if secret := strings.TrimSpace(os.Getenv("BANKING_LOAD_CARD_WEBHOOK_SECRET")); secret != "" {
		body, _ := json.Marshal(map[string]any{
			"id":   "load-ignored-" + time.Now().UTC().Format("20060102150405.000000000"),
			"type": "load.ignored",
			"data": map[string]any{},
		})
		scenarios = append(scenarios, scenario{
			name:   "card-webhook",
			method: http.MethodPost,
			path:   "/v1/card-issuer/webhooks",
			body:   body,
			header: map[string]string{"X-Card-Issuer-Signature": "sha256=" + hmacHex(secret, body)},
		})
	}
	return scenarios
}

func runScenario(client *http.Client, baseURL string, current scenario) error {
	req, err := http.NewRequest(current.method, baseURL+current.path, bytes.NewReader(current.body))
	if err != nil {
		return err
	}
	if len(current.body) > 0 {
		req.Header.Set("Content-Type", "application/json")
	}
	if current.token != "" {
		req.Header.Set("Authorization", "Bearer "+current.token)
	}
	for key, value := range current.header {
		req.Header.Set(key, value)
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
		return &statusError{scenario: current.name, status: resp.StatusCode}
	}
	return nil
}

type statusError struct {
	scenario string
	status   int
}

func (e *statusError) Error() string {
	return e.scenario + " returned status " + strconv.Itoa(e.status)
}

func hmacHex(secret string, body []byte) string {
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write(body)
	return hex.EncodeToString(mac.Sum(nil))
}

func envInt(key string, fallback int) int {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(raw)
	if err != nil || parsed <= 0 {
		return fallback
	}
	return parsed
}
