import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Locale;

/**
 * Harness pengujian end-to-end seluruh API tugas: server dengan filter
 * autentikasi dan klien HttpClient berbagi satu proses yang sama. Dua puluh
 * dua pemeriksaan menutup autentikasi, CRUD, validasi, saldo token, dan
 * persistensi berkas. Exit code 1 bila ada kegagalan.
 */
public class UjiApi {

    /**
     * Kunci penandatanganan untuk pengujian. Di produksi, kunci dibaca dari
     * variabel lingkungan (misalnya System.getenv("API_KEY")) dan tidak
     * pernah ditulis di dalam kode sumber.
     */
    private static final String KUNCI = "kunci-uji-2026";

    private static int lulus = 0;
    private static int gagal = 0;

    public static void main(String[] args) throws Exception {
        Path berkas = Path.of("uji-tugas.txt");
        Files.deleteIfExists(berkas);   // mulai bersih agar uji dapat diulang

        HttpClient klien = HttpClient.newHttpClient();
        String tokenBudi = new TokenAkses("budi", 20).terbitkan(KUNCI);
        String tokenSiti = new TokenAkses("siti", 20).terbitkan(KUNCI);

        System.out.println("== Babak 1: permintaan tanpa token sah ==");
        HttpServer server = buatServer(berkas);
        try {
            String dasar = dasar(server);
            ujiTanpaToken(klien, dasar);
            ujiSkemaSalah(klien, dasar);
            ujiTokenPalsu(klien, dasar);
            ujiMuatanDiganti(klien, dasar, tokenBudi);
            ujiKunciLain(klien, dasar);

            System.out.println();
            System.out.println("== Babak 2: CRUD dan validasi dengan token sah ==");
            ujiBuatBaca(klien, dasar, tokenBudi);
            ujiUbahHapus(klien, dasar, tokenBudi);
            ujiValidasi(klien, dasar, tokenBudi);

            System.out.println();
            System.out.println("== Babak 3: saldo token habis ==");
            ujiSaldo(klien, dasar);
        } finally {
            server.stop(0);
        }

        System.out.println();
        System.out.println("== Babak 4: server baru memuat ulang berkas ==");
        HttpServer serverBaru = buatServer(berkas);
        try {
            ujiPersistensi(klien, dasar(serverBaru), tokenSiti);
        } finally {
            serverBaru.stop(0);
        }

        System.out.println();
        System.out.println("Ringkasan: " + lulus + " lulus, " + gagal + " gagal");
        if (gagal > 0) {
            System.exit(1);
        }
    }

    /** Server uji: router tugas dibungkus filter autentikasi, port dipilih sistem operasi. */
    private static HttpServer buatServer(Path berkas) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
        server.createContext("/", new FilterAuth(
                new RouterTugas(new RepositoryBerkas(berkas)), KUNCI));
        server.start();
        return server;
    }

    /** Alamat dasar server; nomor port tidak dicetak agar keluaran deterministik. */
    private static String dasar(HttpServer server) {
        return "http://127.0.0.1:" + server.getAddress().getPort();
    }

    /** Satu pernyataan uji: catat PASS bila kondisi benar, FAIL bila salah. */
    private static void cek(String nama, boolean kondisi) {
        if (kondisi) {
            lulus++;
            System.out.println("PASS: " + nama);
        } else {
            gagal++;
            System.out.println("FAIL: " + nama);
        }
    }

    /**
     * Mengirim satu permintaan; nilai authorization null berarti tanpa header
     * Authorization sama sekali.
     */
    private static HttpResponse<String> minta(HttpClient klien, String dasar, String authorization,
            String metode, String path, String body) throws IOException, InterruptedException {
        HttpRequest.Builder b = HttpRequest.newBuilder()
                .uri(URI.create(dasar + path))
                .timeout(Duration.ofSeconds(5));
        if (authorization != null) {
            b.header("Authorization", authorization);
        }
        if (body == null) {
            b.method(metode, HttpRequest.BodyPublishers.noBody());
        } else {
            b.header("Content-Type", "application/json")
                    .method(metode, HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
        }
        return klien.send(b.build(), HttpResponse.BodyHandlers.ofString());
    }

    /** Mencetak status dan body respons secara ringkas, tanpa nomor port. */
    private static void lihat(String keterangan, HttpResponse<String> r) {
        System.out.printf(Locale.US, "  %-24s -> %d %s%n",
                keterangan, r.statusCode(), r.body());
    }

    private static void ujiTanpaToken(HttpClient klien, String dasar) throws Exception {
        HttpResponse<String> r = minta(klien, dasar, null, "GET", "/api/tugas", null);
        lihat("tanpa Authorization", r);
        cek("permintaan tanpa token dijawab 401",
                r.statusCode() == 401 && r.body().contains("Authorization"));
        cek("respons 401 menyertakan header WWW-Authenticate",
                r.headers().firstValue("WWW-Authenticate").isPresent());
    }

    private static void ujiSkemaSalah(HttpClient klien, String dasar) throws Exception {
        HttpResponse<String> r = minta(klien, dasar, "Basic teks-asli", "GET", "/api/tugas", null);
        lihat("skema Basic", r);
        cek("skema selain Bearer dijawab 401",
                r.statusCode() == 401 && r.body().contains("Bearer"));
    }

    private static void ujiTokenPalsu(HttpClient klien, String dasar) throws Exception {
        HttpResponse<String> r = minta(klien, dasar, "Bearer bukan-token", "GET", "/api/tugas", null);
        lihat("token tanpa tanda tangan", r);
        cek("token tanpa tanda tangan dijawab 401",
                r.statusCode() == 401 && r.body().contains("format"));
    }

    private static void ujiMuatanDiganti(HttpClient klien, String dasar, String tokenSah)
            throws Exception {
        // Muatan saldo diganti diam-diam menjadi 99; tanda tangan asli dipertahankan.
        String dipalsukan = "budi:99:" + tokenSah.substring(tokenSah.lastIndexOf(':') + 1);
        HttpResponse<String> r = minta(klien, dasar, "Bearer " + dipalsukan, "GET", "/api/tugas", null);
        lihat("muatan diganti", r);
        cek("muatan yang diganti dijawab 401",
                r.statusCode() == 401 && r.body().contains("tanda tangan"));
    }

    private static void ujiKunciLain(HttpClient klien, String dasar) throws Exception {
        String tokenLain = new TokenAkses("budi", 20).terbitkan("kunci-yang-salah");
        HttpResponse<String> r = minta(klien, dasar, "Bearer " + tokenLain, "GET", "/api/tugas", null);
        lihat("kunci penandatangan lain", r);
        cek("token tertanda kunci lain dijawab 401",
                r.statusCode() == 401 && r.body().contains("tanda tangan"));
    }

    private static void ujiBuatBaca(HttpClient klien, String dasar, String token) throws Exception {
        String otorisasi = "Bearer " + token;
        HttpResponse<String> buat = minta(klien, dasar, otorisasi, "POST", "/api/tugas",
                "{\"judul\":\"Belajar HttpServer\",\"pemilik\":\"Budi\","
                        + "\"status\":\"SELESAI\",\"tenggat\":\"2026-01-20\"}");
        cek("POST dengan token sah menghasilkan 201 dan id 1",
                buat.statusCode() == 201 && buat.body().contains("\"id\":1"));
        cek("status SELESAI dari klien diabaikan, server menetapkan TODO",
                buat.body().contains("\"status\":\"TODO\""));

        HttpResponse<String> detail = minta(klien, dasar, otorisasi, "GET", "/api/tugas/1", null);
        cek("GET detail mengembalikan tugas yang tersimpan",
                detail.statusCode() == 200 && detail.body().contains("Belajar HttpServer"));
    }

    private static void ujiUbahHapus(HttpClient klien, String dasar, String token) throws Exception {
        String otorisasi = "Bearer " + token;
        HttpResponse<String> ubah = minta(klien, dasar, otorisasi, "PUT", "/api/tugas/1",
                "{\"judul\":\"Belajar HttpServer\",\"pemilik\":\"Budi\","
                        + "\"status\":\"SELESAI\",\"tenggat\":\"2026-01-20\"}");
        cek("PUT mengubah status menjadi SELESAI",
                ubah.statusCode() == 200 && ubah.body().contains("\"status\":\"SELESAI\""));

        HttpResponse<String> hapus = minta(klien, dasar, otorisasi, "DELETE", "/api/tugas/1", null);
        cek("DELETE menghapus tugas dan menjawab 200",
                hapus.statusCode() == 200 && hapus.body().contains("dihapus"));

        HttpResponse<String> hilang = minta(klien, dasar, otorisasi, "GET", "/api/tugas/1", null);
        cek("tugas yang terhapus dijawab 404",
                hilang.statusCode() == 404 && hilang.body().contains("tidak ditemukan"));

        // Dua tugas baru untuk babak persistensi; id tidak mengulang id yang dihapus.
        minta(klien, dasar, otorisasi, "POST", "/api/tugas",
                "{\"judul\":\"Belajar HttpServer\",\"pemilik\":\"Budi\",\"tenggat\":\"2026-01-20\"}");
        HttpResponse<String> siti = minta(klien, dasar, otorisasi, "POST", "/api/tugas",
                "{\"judul\":\"Tulis dokumentasi API\",\"pemilik\":\"Siti\",\"tenggat\":\"2026-02-01\"}");
        cek("id baru berlanjut tanpa mengulang id terhapus",
                siti.statusCode() == 201 && siti.body().contains("\"id\":3"));

        HttpResponse<String> daftar = minta(klien, dasar, otorisasi, "GET", "/api/tugas", null);
        cek("daftar berisi dua tugas tersisa",
                daftar.statusCode() == 200 && daftar.body().split("\"id\":", -1).length == 3);
    }

    private static void ujiValidasi(HttpClient klien, String dasar, String token) throws Exception {
        String otorisasi = "Bearer " + token;
        HttpResponse<String> tanpaPemilik = minta(klien, dasar, otorisasi, "POST", "/api/tugas",
                "{\"judul\":\"Tugas tanpa pemilik\",\"tenggat\":\"2026-01-22\"}");
        cek("payload tanpa pemilik dijawab 400",
                tanpaPemilik.statusCode() == 400
                        && tanpaPemilik.body().contains("pemilik wajib diisi"));

        HttpResponse<String> rusak = minta(klien, dasar, otorisasi, "POST", "/api/tugas",
                "{\"judul\":");
        cek("JSON rusak dijawab 400",
                rusak.statusCode() == 400 && rusak.body().contains("JSON tidak valid"));

        HttpResponse<String> tambal = minta(klien, dasar, otorisasi, "PATCH", "/api/tugas/2", null);
        cek("PATCH dijawab 405",
                tambal.statusCode() == 405 && tambal.body().contains("PATCH"));

        HttpResponse<String> asing = minta(klien, dasar, otorisasi, "GET", "/api/pengguna", null);
        cek("endpoint tidak dikenal dijawab 404",
                asing.statusCode() == 404 && asing.body().contains("endpoint"));
    }

    private static void ujiSaldo(HttpClient klien, String dasar) throws Exception {
        String tokenKuota = new TokenAkses("kuota", 2).terbitkan(KUNCI);
        HttpResponse<String> pertama = minta(klien, dasar, "Bearer " + tokenKuota,
                "GET", "/api/tugas", null);
        HttpResponse<String> kedua = minta(klien, dasar, "Bearer " + tokenKuota,
                "GET", "/api/tugas/2", null);
        cek("token bersaldo 2 melayani dua permintaan pertama",
                pertama.statusCode() == 200 && kedua.statusCode() == 200);

        HttpResponse<String> ketiga = minta(klien, dasar, "Bearer " + tokenKuota,
                "GET", "/api/tugas", null);
        lihat("permintaan ketiga", ketiga);
        cek("permintaan ketiga dijawab 401 karena saldo habis",
                ketiga.statusCode() == 401 && ketiga.body().contains("saldo"));
    }

    private static void ujiPersistensi(HttpClient klien, String dasar, String token) throws Exception {
        String otorisasi = "Bearer " + token;
        HttpResponse<String> daftar = minta(klien, dasar, otorisasi, "GET", "/api/tugas", null);
        cek("tugas bertahan melewati restart server",
                daftar.statusCode() == 200 && daftar.body().contains("Tulis dokumentasi API"));

        HttpResponse<String> lanjutan = minta(klien, dasar, otorisasi, "POST", "/api/tugas",
                "{\"judul\":\"Rapat tim mingguan\",\"pemilik\":\"Dewi\",\"tenggat\":\"2026-02-10\"}");
        cek("id berlanjut dari isi berkas setelah restart",
                lanjutan.statusCode() == 201 && lanjutan.body().contains("\"id\":4"));
    }
}
