// Test black-box untuk API katalog.
//
// Test ini tidak memanggil fungsi internal aplikasi; yang diperiksa adalah
// perilaku HTTP persis seperti yang dirasakan klien sungguhan. Aplikasi
// harus sudah berjalan di APP_URL (default http://localhost:8080) dengan
// database yang dapat dijangkau lewat TEST_DATABASE_URL atau variabel PG*.
package tests

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/jackc/pgx/v5"
)

// appURL adalah alamat aplikasi yang sedang diuji.
var appURL string

// TestMain memastikan aplikasi siap sebelum seluruh test dijalankan.
func TestMain(m *testing.M) {
	appURL = os.Getenv("APP_URL")
	if appURL == "" {
		appURL = "http://localhost:8080"
	}
	if err := tungguSiap(30 * time.Second); err != nil {
		fmt.Println("aplikasi tidak siap:", err)
		os.Exit(1)
	}
	os.Exit(m.Run())
}

// tungguSiap melakukan polling /healthz sampai aplikasi merespons
// atau batas waktu terlampaui — container sering butuh beberapa detik
// sebelum benar-benar siap menerima request.
func tungguSiap(batas time.Duration) error {
	mulai := time.Now()
	for {
		resp, err := http.Get(appURL + "/healthz")
		if err == nil {
			resp.Body.Close()
			if resp.StatusCode == http.StatusOK {
				return nil
			}
		}
		if time.Since(mulai) > batas {
			return fmt.Errorf("timeout menunggu %s/healthz setelah %s", appURL, batas)
		}
		time.Sleep(500 * time.Millisecond)
	}
}

// dsnTest menyusun DSN untuk reset database: TEST_DATABASE_URL diutamakan,
// jika kosong variabel PG* dirangkai dengan nilai default lokal.
func dsnTest() string {
	if dsn := os.Getenv("TEST_DATABASE_URL"); dsn != "" {
		return dsn
	}
	ambil := func(kunci, standar string) string {
		if v := os.Getenv(kunci); v != "" {
			return v
		}
		return standar
	}
	return fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable",
		ambil("PGHOST", "localhost"),
		ambil("PGPORT", "5432"),
		ambil("PGUSER", "katalog"),
		ambil("PGDATABASE", "katalog"),
	)
}

// resetItems mengosongkan tabel items dan mengembalikan counter id ke 1
// sehingga setiap test mulai dari kondisi yang sama.
func resetItems(t *testing.T) {
	t.Helper()
	ctx := context.Background()
	conn, err := pgx.Connect(ctx, dsnTest())
	if err != nil {
		t.Fatalf("koneksi database untuk reset: %v", err)
	}
	defer conn.Close(ctx)
	if _, err := conn.Exec(ctx, `TRUNCATE items RESTART IDENTITY`); err != nil {
		t.Fatalf("reset tabel items: %v", err)
	}
}

// postJSON mengirim body JSON dan mengembalikan respons mentahnya.
func postJSON(t *testing.T, path string, body any) *http.Response {
	t.Helper()
	isi, err := json.Marshal(body)
	if err != nil {
		t.Fatalf("marshal body: %v", err)
	}
	resp, err := http.Post(appURL+path, "application/json", bytes.NewReader(isi))
	if err != nil {
		t.Fatalf("POST %s: %v", path, err)
	}
	t.Cleanup(func() { resp.Body.Close() })
	return resp
}

// getJSON mengirim GET dan mengurai hasilnya ke target.
func getJSON(t *testing.T, path string, target any) *http.Response {
	t.Helper()
	resp, err := http.Get(appURL + path)
	if err != nil {
		t.Fatalf("GET %s: %v", path, err)
	}
	t.Cleanup(func() { resp.Body.Close() })
	if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
		t.Fatalf("decode respons GET %s: %v", path, err)
	}
	return resp
}

func TestHealthz(t *testing.T) {
	var body struct {
		Status string `json:"status"`
	}
	resp := getJSON(t, "/healthz", &body)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, ingin 200", resp.StatusCode)
	}
	if body.Status != "ok" {
		t.Fatalf("status body = %q, ingin \"ok\"", body.Status)
	}
}

func TestCreateListGetItem(t *testing.T) {
	resetItems(t)

	t.Run("create", func(t *testing.T) {
		resp := postJSON(t, "/api/items", map[string]string{
			"name": "Kopi Torabika",
			"note": "untuk pengujian",
		})
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("status = %d, ingin 201", resp.StatusCode)
		}
		var it struct {
			ID   int64  `json:"id"`
			Name string `json:"name"`
			Note string `json:"note"`
		}
		if err := json.NewDecoder(resp.Body).Decode(&it); err != nil {
			t.Fatalf("decode item: %v", err)
		}
		if it.ID != 1 {
			t.Fatalf("id = %d, ingin 1 (tabel baru di-reset)", it.ID)
		}
		if it.Name != "Kopi Torabika" {
			t.Fatalf("name = %q, ingin \"Kopi Torabika\"", it.Name)
		}
	})

	t.Run("list", func(t *testing.T) {
		var items []struct {
			ID   int64  `json:"id"`
			Name string `json:"name"`
		}
		resp := getJSON(t, "/api/items", &items)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, ingin 200", resp.StatusCode)
		}
		if len(items) != 1 || items[0].Name != "Kopi Torabika" {
			t.Fatalf("list = %+v, ingin satu item Kopi Torabika", items)
		}
	})

	t.Run("get", func(t *testing.T) {
		var it struct {
			ID   int64  `json:"id"`
			Name string `json:"name"`
			Note string `json:"note"`
		}
		resp := getJSON(t, "/api/items/1", &it)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, ingin 200", resp.StatusCode)
		}
		if it.Name != "Kopi Torabika" || it.Note != "untuk pengujian" {
			t.Fatalf("item = %+v, name/note tidak sesuai", it)
		}
	})
}

func TestGetItemNotFound(t *testing.T) {
	resetItems(t)
	var body map[string]string
	resp := getJSON(t, "/api/items/999", &body)
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("status = %d, ingin 404", resp.StatusCode)
	}
	if body["error"] == "" {
		t.Fatal("respons 404 seharusnya memuat pesan error")
	}
}

func TestCreateItemValidasi(t *testing.T) {
	resetItems(t)

	t.Run("nama kosong", func(t *testing.T) {
		resp := postJSON(t, "/api/items", map[string]string{
			"name": "",
			"note": "",
		})
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, ingin 400", resp.StatusCode)
		}
	})

	t.Run("catatan terlalu panjang", func(t *testing.T) {
		resp := postJSON(t, "/api/items", map[string]string{
			"name": "Kopi Gayo",
			"note": strings.Repeat("x", 201),
		})
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, ingin 400", resp.StatusCode)
		}
	})
}
