package main

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

// waktuUji adalah waktu beku untuk seluruh unit test API.
var waktuUji = time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)

// serverUji membangun API lengkap di atas MemoryItemRepository lalu
// menjalankannya di httptest.Server. Tidak ada database dan tidak ada
// port yang perlu dikelola manual.
func serverUji() *httptest.Server {
	svc := NewItemService(NewMemoryItemRepository(), FixedClock{Waktu: waktuUji})
	return httptest.NewServer(NewRouter(svc))
}

// bacaEnvelope mendekode body {"data": ...} menjadi struct.
func bacaEnvelope[T any](t *testing.T, resp *http.Response) T {
	t.Helper()
	var env struct {
		Data T `json:"data"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
		t.Fatalf("mendekode body: %v", err)
	}
	return env.Data
}

// bacaGalat mendekode body {"error": ...} menjadi errorBody.
func bacaGalat(t *testing.T, resp *http.Response) errorBody {
	t.Helper()
	var env errorEnvelope
	if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
		t.Fatalf("mendekode body galat: %v", err)
	}
	return env.Error
}

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

	resp, err := http.Post(ts.URL+"/items", "application/json",
		strings.NewReader(`{"name":"Buku Go","price":75000}`))
	if err != nil {
		t.Fatalf("POST /items: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("POST /items status = %d, ingin 201", resp.StatusCode)
	}
	if loc := resp.Header.Get("Location"); loc != "/items/1" {
		t.Errorf("Location = %q, ingin /items/1", loc)
	}
	item := bacaEnvelope[Item](t, resp)
	if item.ID != 1 || item.Name != "Buku Go" || item.Price != 75000 {
		t.Errorf("item tersimpan = %+v, tidak sesuai masukan", item)
	}
	if !item.CreatedAt.Equal(waktuUji) {
		t.Errorf("created_at = %v, ingin %v (waktu beku)", item.CreatedAt, waktuUji)
	}

	resp2, err := http.Get(ts.URL + "/items/1")
	if err != nil {
		t.Fatalf("GET /items/1: %v", err)
	}
	defer resp2.Body.Close()
	if resp2.StatusCode != http.StatusOK {
		t.Fatalf("GET /items/1 status = %d, ingin 200", resp2.StatusCode)
	}
	lagi := bacaEnvelope[Item](t, resp2)
	if lagi != item {
		t.Errorf("item terbaca = %+v, ingin %+v", lagi, item)
	}
}

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

	resp, err := http.Post(ts.URL+"/items", "application/json",
		strings.NewReader(`{"name":"Mouse","price":150000}`))
	if err != nil {
		t.Fatalf("POST /items: %v", err)
	}
	resp.Body.Close()

	req, err := http.NewRequest(http.MethodPut, ts.URL+"/items/1",
		strings.NewReader(`{"name":"Mouse Nirkabel","price":175000}`))
	if err != nil {
		t.Fatalf("membuat permintaan PUT: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp2, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("PUT /items/1: %v", err)
	}
	defer resp2.Body.Close()
	if resp2.StatusCode != http.StatusOK {
		t.Fatalf("PUT /items/1 status = %d, ingin 200", resp2.StatusCode)
	}
	item := bacaEnvelope[Item](t, resp2)
	if item.Name != "Mouse Nirkabel" || item.Price != 175000 {
		t.Errorf("item setelah PUT = %+v, ingin data baru", item)
	}
	if !item.CreatedAt.Equal(waktuUji) {
		t.Errorf("created_at berubah setelah PUT: %v", item.CreatedAt)
	}

	reqDel, err := http.NewRequest(http.MethodDelete, ts.URL+"/items/1", nil)
	if err != nil {
		t.Fatalf("membuat permintaan DELETE: %v", err)
	}
	resp3, err := http.DefaultClient.Do(reqDel)
	if err != nil {
		t.Fatalf("DELETE /items/1: %v", err)
	}
	defer resp3.Body.Close()
	if resp3.StatusCode != http.StatusNoContent {
		t.Fatalf("DELETE /items/1 status = %d, ingin 204", resp3.StatusCode)
	}

	resp4, err := http.Get(ts.URL + "/items/1")
	if err != nil {
		t.Fatalf("GET /items/1: %v", err)
	}
	defer resp4.Body.Close()
	if resp4.StatusCode != http.StatusNotFound {
		t.Fatalf("GET setelah DELETE status = %d, ingin 404", resp4.StatusCode)
	}
	galat := bacaGalat(t, resp4)
	if galat.Code != "NOT_FOUND" {
		t.Errorf("kode galat = %q, ingin NOT_FOUND", galat.Code)
	}
}

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

	resp, err := http.Post(ts.URL+"/items", "application/json",
		strings.NewReader(`{"name":"","price":-100}`))
	if err != nil {
		t.Fatalf("POST /items: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, ingin 400", resp.StatusCode)
	}
	galat := bacaGalat(t, resp)
	if galat.Code != "VALIDATION_ERROR" {
		t.Errorf("kode galat = %q, ingin VALIDATION_ERROR", galat.Code)
	}
	if galat.Fields["name"] != "wajib diisi" {
		t.Errorf("fields[name] = %q, ingin pesan wajib diisi", galat.Fields["name"])
	}
	if galat.Fields["price"] != "tidak boleh negatif" {
		t.Errorf("fields[price] = %q, ingin pesan tidak boleh negatif", galat.Fields["price"])
	}
}

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

	resp, err := http.Post(ts.URL+"/items", "text/plain",
		strings.NewReader(`{"name":"Buku"}`))
	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+"/items", "application/json",
		strings.NewReader(`{nama: "buku"}`))
	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.Get(ts.URL + "/items/abc")
	if err != nil {
		t.Fatalf("GET /items/abc: %v", err)
	}
	resp3.Body.Close()
	if resp3.StatusCode != http.StatusBadRequest {
		t.Errorf("id bukan angka status = %d, ingin 400", resp3.StatusCode)
	}
}

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

	resp, err := http.Get(ts.URL + "/items/999")
	if err != nil {
		t.Fatalf("GET /items/999: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("status = %d, ingin 404", resp.StatusCode)
	}
	galat := bacaGalat(t, resp)
	if galat.Code != "NOT_FOUND" {
		t.Errorf("kode galat = %q, ingin NOT_FOUND", galat.Code)
	}
}
