package main

import (
	"bytes"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// newTestServer merakit server uji dari komponen produksi yang sama:
// handler, router, dan middleware chain. httptest.NewServer menjalankan
// server sungguhan di alamat localhost acak — server produksi tidak ikut
// listen di port berapa pun selama pengujian.
func newTestServer(t *testing.T, staticDir string, logger io.Writer) *httptest.Server {
	t.Helper()

	handlers, err := NewHandlers()
	if err != nil {
		t.Fatalf("NewHandlers() galat: %v", err)
	}

	mux := NewRouter(handlers, staticDir)
	handler := Chain(mux,
		NewRequestID(),
		NewLogging(logger),
		NewRecover(logger),
	)

	ts := httptest.NewServer(handler)
	t.Cleanup(ts.Close)
	return ts
}

// buatStaticDir membuat direktori statis sementara berisi style.css.
// Pengujian tidak boleh bergantung pada berkas di folder proyek.
func buatStaticDir(t *testing.T) string {
	t.Helper()

	dir := t.TempDir()
	isi := "body { margin: 0; }\n"
	if err := os.WriteFile(filepath.Join(dir, "style.css"), []byte(isi), 0o644); err != nil {
		t.Fatalf("menulis style.css gagal: %v", err)
	}
	return dir
}

func TestHomeMerenderHalaman(t *testing.T) {
	ts := newTestServer(t, buatStaticDir(t), io.Discard)

	resp, err := http.Get(ts.URL + "/")
	if err != nil {
		t.Fatalf("GET / gagal: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	if resp.StatusCode != http.StatusOK {
		t.Errorf("status GET / = %d, ingin 200", resp.StatusCode)
	}
	if !strings.Contains(string(body), "Selamat Datang di Web Server") {
		t.Errorf("body home tidak memuat judul halaman: %q", body)
	}
	if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
		t.Errorf("Content-Type home = %q, ingin text/html", ct)
	}
}

func TestHealthzMenjawabOk(t *testing.T) {
	ts := newTestServer(t, buatStaticDir(t), io.Discard)

	resp, err := http.Get(ts.URL + "/healthz")
	if err != nil {
		t.Fatalf("GET /healthz gagal: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	if resp.StatusCode != http.StatusOK {
		t.Errorf("status GET /healthz = %d, ingin 200", resp.StatusCode)
	}
	if strings.TrimSpace(string(body)) != "ok" {
		t.Errorf("body /healthz = %q, ingin \"ok\"", body)
	}
}

func TestKontakMenampilkanEmail(t *testing.T) {
	ts := newTestServer(t, buatStaticDir(t), io.Discard)

	resp, err := http.Get(ts.URL + "/kontak")
	if err != nil {
		t.Fatalf("GET /kontak gagal: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	if resp.StatusCode != http.StatusOK {
		t.Errorf("status GET /kontak = %d, ingin 200", resp.StatusCode)
	}
	if !strings.Contains(string(body), "budi@example.com") {
		t.Errorf("body kontak tidak memuat email: %q", body)
	}
}

func TestRuteTidakDikenalMenjawab404(t *testing.T) {
	ts := newTestServer(t, buatStaticDir(t), io.Discard)

	resp, err := http.Get(ts.URL + "/tidak-ada")
	if err != nil {
		t.Fatalf("GET /tidak-ada gagal: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("status GET /tidak-ada = %d, ingin 404", resp.StatusCode)
	}
}

func TestMetodeSalahMenjawab405(t *testing.T) {
	ts := newTestServer(t, buatStaticDir(t), io.Discard)

	resp, err := http.Post(ts.URL+"/healthz", "text/plain", strings.NewReader("tes"))
	if err != nil {
		t.Fatalf("POST /healthz gagal: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusMethodNotAllowed {
		t.Errorf("status POST /healthz = %d, ingin 405", resp.StatusCode)
	}
}

func TestStaticFileDilayaniDariDirektoriUji(t *testing.T) {
	ts := newTestServer(t, buatStaticDir(t), io.Discard)

	resp, err := http.Get(ts.URL + "/static/style.css")
	if err != nil {
		t.Fatalf("GET /static/style.css gagal: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	if resp.StatusCode != http.StatusOK {
		t.Errorf("status GET /static/style.css = %d, ingin 200", resp.StatusCode)
	}
	if !strings.Contains(string(body), "margin: 0") {
		t.Errorf("body style.css tidak berisi CSS uji: %q", body)
	}
}

func TestRequestIDHeaderTersediaDanBerurutan(t *testing.T) {
	ts := newTestServer(t, buatStaticDir(t), io.Discard)

	resp1, err := http.Get(ts.URL + "/healthz")
	if err != nil {
		t.Fatalf("permintaan pertama gagal: %v", err)
	}
	resp1.Body.Close()
	resp2, err := http.Get(ts.URL + "/healthz")
	if err != nil {
		t.Fatalf("permintaan kedua gagal: %v", err)
	}
	resp2.Body.Close()

	if got := resp1.Header.Get("X-Request-ID"); got != "req-001" {
		t.Errorf("X-Request-ID permintaan pertama = %q, ingin req-001", got)
	}
	if got := resp2.Header.Get("X-Request-ID"); got != "req-002" {
		t.Errorf("X-Request-ID permintaan kedua = %q, ingin req-002", got)
	}
}

func TestRecoverMengubahPanicMenjadi500(t *testing.T) {
	var log bytes.Buffer

	// Handler uji sengaja panic; hanya middleware Recover yang membungkusnya.
	panicHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		panic("kegagalan sengaja untuk pengujian")
	})
	ts := httptest.NewServer(Chain(panicHandler, NewRecover(&log)))
	t.Cleanup(ts.Close)

	resp, err := http.Get(ts.URL + "/")
	if err != nil {
		t.Fatalf("GET / ke handler panic gagal: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	if resp.StatusCode != http.StatusInternalServerError {
		t.Errorf("status = %d, ingin 500", resp.StatusCode)
	}
	if !strings.Contains(string(body), "kesalahan internal server") {
		t.Errorf("body respons panic = %q", body)
	}
	if !strings.Contains(log.String(), "PANIC") {
		t.Errorf("log tidak mencatat panic: %q", log.String())
	}
}

func TestLoggingMencatatPermintaan(t *testing.T) {
	var log bytes.Buffer
	ts := newTestServer(t, buatStaticDir(t), &log)

	resp, err := http.Get(ts.URL + "/healthz")
	if err != nil {
		t.Fatalf("GET /healthz gagal: %v", err)
	}
	resp.Body.Close()

	want := "req-001 GET /healthz 200"
	if !strings.Contains(log.String(), want) {
		t.Errorf("log = %q, ingin memuat %q", log.String(), want)
	}
}
