package asset

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"net/url"
	"sync/atomic"
	"testing"
	"time"
)

func TestStatusErrorMessage(t *testing.T) {
	cases := map[int]string{
		403: "HTTP 403 Forbidden",
		404: "HTTP 404 Not Found",
		999: "HTTP 999",
	}
	for code, want := range cases {
		if got := (&StatusError{Code: code}).Error(); got != want {
			t.Errorf("StatusError{%d} = %q; want %q", code, got, want)
		}
	}
}

func TestGetRetriesTransientThenSucceeds(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 403 on the first try (like bot-protection), then serve the file.
		if atomic.AddInt32(&hits, 1) == 1 {
			w.WriteHeader(http.StatusForbidden)
			return
		}
		w.Header().Set("Content-Type", "text/css")
		_, _ = w.Write([]byte("body{}"))
	}))
	defer srv.Close()

	d := NewDownloader("kage-test", 5*time.Second, 0)
	u, _ := url.Parse(srv.URL + "/style.css")
	res, err := d.Get(context.Background(), u, "")
	if err != nil {
		t.Fatalf("Get after retry: %v", err)
	}
	if !res.IsCSS || string(res.Body) != "body{}" {
		t.Errorf("unexpected result: css=%v body=%q", res.IsCSS, res.Body)
	}
	if hits < 2 {
		t.Errorf("expected a retry; server saw %d hits", hits)
	}
}

func TestGetDoesNotRetryPermanent(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&hits, 1)
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	d := NewDownloader("kage-test", 5*time.Second, 0)
	u, _ := url.Parse(srv.URL + "/missing.png")
	_, err := d.Get(context.Background(), u, "")

	var se *StatusError
	if !errors.As(err, &se) || se.Code != 404 {
		t.Fatalf("got %v; want StatusError 404", err)
	}
	if hits != 1 {
		t.Errorf("404 should not be retried; server saw %d hits", hits)
	}
}

func TestGetGivesUpAfterRetries(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&hits, 1)
		w.WriteHeader(http.StatusTooManyRequests)
	}))
	defer srv.Close()

	d := NewDownloader("kage-test", 5*time.Second, 0)
	d.Retries = 2
	u, _ := url.Parse(srv.URL + "/rate.css")
	_, err := d.Get(context.Background(), u, "")

	var se *StatusError
	if !errors.As(err, &se) || se.Code != 429 {
		t.Fatalf("got %v; want StatusError 429", err)
	}
	if hits != 3 { // 1 try + 2 retries
		t.Errorf("expected 3 attempts, server saw %d", hits)
	}
}

func TestGetSkipsOverCapByContentLength(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&hits, 1)
		body := make([]byte, 4096) // declared via Content-Length
		w.Header().Set("Content-Type", "video/mp4")
		_, _ = w.Write(body)
	}))
	defer srv.Close()

	d := NewDownloader("kage-test", 5*time.Second, 1024) // cap below the body
	u, _ := url.Parse(srv.URL + "/clip.mp4")
	_, err := d.Get(context.Background(), u, "")
	if !errors.Is(err, ErrTooLarge) {
		t.Fatalf("got %v; want ErrTooLarge", err)
	}
	if hits != 1 {
		t.Errorf("an over-cap asset should not be retried; server saw %d hits", hits)
	}
}

func TestGetSkipsOverCapWithoutContentLength(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// A chunked response carries no Content-Length, so the cap is only known
		// after reading past it.
		w.Header().Set("Content-Type", "application/octet-stream")
		fl, _ := w.(http.Flusher)
		chunk := make([]byte, 512)
		for i := 0; i < 8; i++ {
			_, _ = w.Write(chunk)
			if fl != nil {
				fl.Flush()
			}
		}
	}))
	defer srv.Close()

	d := NewDownloader("kage-test", 5*time.Second, 1024)
	u, _ := url.Parse(srv.URL + "/stream.bin")
	_, err := d.Get(context.Background(), u, "")
	if !errors.Is(err, ErrTooLarge) {
		t.Fatalf("got %v; want ErrTooLarge", err)
	}
}

func TestGetKeepsUnderCap(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "image/png")
		_, _ = w.Write([]byte("small"))
	}))
	defer srv.Close()

	d := NewDownloader("kage-test", 5*time.Second, 1024)
	u, _ := url.Parse(srv.URL + "/logo.png")
	res, err := d.Get(context.Background(), u, "")
	if err != nil {
		t.Fatalf("under-cap asset: %v", err)
	}
	if string(res.Body) != "small" {
		t.Errorf("body = %q; want %q", res.Body, "small")
	}
}

func TestTransientClassification(t *testing.T) {
	transientCodes := []int{403, 408, 425, 429, 500, 502, 503}
	for _, c := range transientCodes {
		if !transient(&StatusError{Code: c}) {
			t.Errorf("status %d should be transient", c)
		}
	}
	for _, c := range []int{400, 401, 404, 410} {
		if transient(&StatusError{Code: c}) {
			t.Errorf("status %d should be permanent", c)
		}
	}
	if transient(context.Canceled) {
		t.Error("context.Canceled should not be transient")
	}
}
