package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// klienTanpaIkut redirect membiarkan respons 302 sampai ke test
// alih-alih diikuti otomatis oleh http.Client.
var klienTanpaIkut = &http.Client{
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse
	},
}

// serverUji membangun layanan lengkap di atas MemoryURLRepository
// dengan generator stub dan waktu beku, lalu menjalankannya di
// httptest.Server.
func serverUji() *httptest.Server {
	gen := &genStub{daftar: []string{"b1Gk0D3"}}
	svc := NewShortenService(NewMemoryURLRepository(), gen, FixedClock{Waktu: waktuUji})
	return httptest.NewServer(NewRouter(svc, "http://short.test/"))
}

func TestAlurShortenRedirectStats(t *testing.T) {
	ts := serverUji()
	defer ts.Close()

	resp, err := http.Post(ts.URL+"/shorten", "application/json",
		strings.NewReader(`{"url":"https://example.com/tutorial-go"}`))
	if err != nil {
		t.Fatalf("POST /shorten: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("POST /shorten status = %d, ingin 201", resp.StatusCode)
	}
	if loc := resp.Header.Get("Location"); loc != "http://short.test/b1Gk0D3" {
		t.Errorf("Location = %q, ingin short URL", loc)
	}
	var balas struct {
		Code     string `json:"code"`
		URL      string `json:"url"`
		ShortURL string `json:"short_url"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&balas); err != nil {
		t.Fatalf("mendekode body: %v", err)
	}
	if balas.Code != "b1Gk0D3" || balas.URL != "https://example.com/tutorial-go" {
		t.Errorf("balasan = %+v, tidak sesuai masukan", balas)
	}
	if balas.ShortURL != "http://short.test/b1Gk0D3" {
		t.Errorf("short_url = %q, ingin http://short.test/b1Gk0D3", balas.ShortURL)
	}

	// Dua redirect: masing-masing 302 + Location URL asli.
	for i := 1; i <= 2; i++ {
		resp2, err := klienTanpaIkut.Get(ts.URL + "/b1Gk0D3")
		if err != nil {
			t.Fatalf("GET /b1Gk0D3 ke-%d: %v", i, err)
		}
		resp2.Body.Close()
		if resp2.StatusCode != http.StatusFound {
			t.Fatalf("redirect ke-%d status = %d, ingin 302", i, resp2.StatusCode)
		}
		if loc := resp2.Header.Get("Location"); loc != "https://example.com/tutorial-go" {
			t.Errorf("redirect ke-%d Location = %q, ingin URL asli", i, loc)
		}
	}

	resp3, err := http.Get(ts.URL + "/stats/b1Gk0D3")
	if err != nil {
		t.Fatalf("GET /stats/b1Gk0D3: %v", err)
	}
	defer resp3.Body.Close()
	if resp3.StatusCode != http.StatusOK {
		t.Fatalf("GET /stats status = %d, ingin 200", resp3.StatusCode)
	}
	var stat struct {
		Code      string    `json:"code"`
		URL       string    `json:"url"`
		Clicks    int64     `json:"clicks"`
		CreatedAt time.Time `json:"created_at"`
	}
	if err := json.NewDecoder(resp3.Body).Decode(&stat); err != nil {
		t.Fatalf("mendekode body stats: %v", err)
	}
	if stat.Clicks != 2 {
		t.Errorf("klik = %d, ingin 2 setelah dua redirect", stat.Clicks)
	}
	if !stat.CreatedAt.Equal(waktuUji) {
		t.Errorf("created_at = %v, ingin %v (waktu beku)", stat.CreatedAt, waktuUji)
	}
}

func TestShortenBodyTidakValid(t *testing.T) {
	ts := serverUji()
	defer ts.Close()

	resp, err := http.Post(ts.URL+"/shorten", "text/plain", strings.NewReader(`{}`))
	if err != nil {
		t.Fatalf("POST text/plain: %v", err)
	}
	resp.Body.Close()
	if resp.StatusCode != http.StatusUnsupportedMediaType {
		t.Errorf("Content-Type salah status = %d, ingin 415", resp.StatusCode)
	}

	resp2, err := http.Post(ts.URL+"/shorten", "application/json", strings.NewReader(`bukan json`))
	if err != nil {
		t.Fatalf("POST JSON rusak: %v", err)
	}
	resp2.Body.Close()
	if resp2.StatusCode != http.StatusBadRequest {
		t.Errorf("JSON rusak status = %d, ingin 400", resp2.StatusCode)
	}

	resp3, err := http.Post(ts.URL+"/shorten", "application/json",
		strings.NewReader(`{"url":"example.com/tanpa-skema"}`))
	if err != nil {
		t.Fatalf("POST URL tanpa skema: %v", err)
	}
	defer resp3.Body.Close()
	if resp3.StatusCode != http.StatusBadRequest {
		t.Fatalf("URL tanpa skema status = %d, ingin 400", resp3.StatusCode)
	}
	var env struct {
		Error struct {
			Code   string            `json:"code"`
			Fields map[string]string `json:"fields"`
		} `json:"error"`
	}
	if err := json.NewDecoder(resp3.Body).Decode(&env); err != nil {
		t.Fatalf("mendekode body galat: %v", err)
	}
	if env.Error.Code != "VALIDATION_ERROR" {
		t.Errorf("kode galat = %q, ingin VALIDATION_ERROR", env.Error.Code)
	}
	if env.Error.Fields["url"] == "" {
		t.Error("fields[url] kosong, ingin pesan galat skema")
	}
}

func TestKodeTakDikenal(t *testing.T) {
	ts := serverUji()
	defer ts.Close()

	resp, err := klienTanpaIkut.Get(ts.URL + "/zzzzzzz")
	if err != nil {
		t.Fatalf("GET /zzzzzzz: %v", err)
	}
	resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("GET kode tak dikenal status = %d, ingin 404", resp.StatusCode)
	}

	resp2, err := http.Get(ts.URL + "/stats/zzzzzzz")
	if err != nil {
		t.Fatalf("GET /stats/zzzzzzz: %v", err)
	}
	defer resp2.Body.Close()
	if resp2.StatusCode != http.StatusNotFound {
		t.Errorf("GET stats kode tak dikenal status = %d, ingin 404", resp2.StatusCode)
	}
}
