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.time.LocalDate;
import java.util.Base64;
import java.util.List;

/**
 * Harness pengujian bertingkat aplikasi web manajemen tugas. Tingkat 1
 * menguji unit tanpa jaringan: ValidasiWeb, LayananTugasWeb dengan repository
 * memori, dan RepositoryTugasWebBerkas. Tingkat 2 menguji integrasi HTTP
 * in-process: FilterTokenWeb membungkus RouterWeb di atas HttpServer yang
 * mendengarkan 127.0.0.1 pada port pilihan sistem operasi (nomor port tidak
 * dicetak agar keluaran deterministik), lalu satu HttpClient mengirim seluruh
 * permintaan secara sekuensial. Exit code 1 bila ada kegagalan.
 */
public class UjiAplikasiWeb {

    /** Hari operasional beku: aturan "tenggat lampau" diuji tanpa LocalDate.now(). */
    private static final LocalDate HARI = LocalDate.of(2026, 1, 15);

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

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

        System.out.println("== Tingkat 1: unit tanpa jaringan ==");
        ujiValidasi();
        ujiLayanan();
        ujiRepositoryBerkas(berkasRepo);

        System.out.println();
        System.out.println("== Tingkat 2: integrasi HTTP dengan filter aktif ==");
        HttpClient klien = HttpClient.newHttpClient();
        HttpServer server = mulai(berkasIntegrasi);
        try {
            String dasar = dasar(server);
            ujiAutentikasi(klien, dasar);
            ujiCrud(klien, dasar);
        } finally {
            server.stop(0);
        }

        System.out.println();
        System.out.println("== Tingkat 2 (lanjutan): server baru memuat berkas ==");
        HttpServer serverBaru = mulai(berkasIntegrasi);
        try {
            ujiPersistensi(klien, dasar(serverBaru));
        } finally {
            serverBaru.stop(0);
        }

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

    /** Tingkat 1a: validasi bentuk field mengumpulkan galat, bukan berhenti di awal. */
    private static void ujiValidasi() {
        List<String> sah = ValidasiWeb.periksa(new PermintaanTugas(
                "Belajar arsitektur berlapis", "Budi", "2026-02-01"));
        periksa("validasi: muatan sah tidak memuat galat", sah.isEmpty());

        List<String> judul = ValidasiWeb.periksa(new PermintaanTugas(
                "ab", "Budi", "2026-02-01"));
        periksa("validasi: judul terlalu pendek ditolak",
                judul.size() == 1 && judul.get(0).startsWith("judul"));

        List<String> pemilik = ValidasiWeb.periksa(new PermintaanTugas(
                "Menulis dokumentasi API", "", "2026-02-01"));
        periksa("validasi: pemilik kosong ditolak",
                pemilik.size() == 1 && pemilik.get(0).startsWith("pemilik"));

        List<String> tenggat = ValidasiWeb.periksa(new PermintaanTugas(
                "Menulis dokumentasi API", "Budi", "besok"));
        periksa("validasi: tenggat bukan format ISO ditolak",
                tenggat.size() == 1 && tenggat.get(0).startsWith("tenggat"));

        List<String> pipa = ValidasiWeb.periksa(new PermintaanTugas(
                "Riset|keamanan", "Budi", "2026-02-01"));
        periksa("validasi: karakter pipa ditolak",
                pipa.size() == 1 && pipa.get(0).contains("|"));

        List<String> semuaSalah = ValidasiWeb.periksa(new PermintaanTugas(
                "ab", "", "besok"));
        periksa("validasi: tiga galat terkumpul sekaligus", semuaSalah.size() == 3);
    }

    /** Tingkat 1b: aturan bisnis layanan dengan repository memori dan hari beku. */
    private static void ujiLayanan() {
        LayananTugasWeb layanan = new LayananTugasWeb(
                new RepositoryTugasWebMemori(), HARI);

        TugasWeb tugas = layanan.buat(new PermintaanTugas(
                "Belajar arsitektur berlapis", "Budi", "2026-02-01"));
        periksa("layanan: buat menetapkan status TODO",
                tugas.status() == StatusTugasWeb.TODO);
        periksa("layanan: buat menetapkan id mulai 1", tugas.id() == 1);

        periksa("layanan: tenggat lampau ditolak status 400",
                menolak(() -> layanan.buat(new PermintaanTugas(
                        "Riset keamanan", "Dewi", "2026-01-01")),
                        ValidasiGagalException.class, 400));

        TugasWeb selesai = layanan.tandaiSelesai(1);
        periksa("layanan: tandaiSelesai memindahkan status ke SELESAI",
                selesai.status() == StatusTugasWeb.SELESAI);
        periksa("layanan: tandaiSelesai kedua kali ditolak status 409",
                menolak(() -> layanan.tandaiSelesai(1),
                        AturanBisnisException.class, 409));

        TugasWeb revisi = layanan.ubah(1, new PermintaanTugas(
                "Belajar arsitektur berlapis (revisi)", "Budi", "2026-02-08"));
        periksa("layanan: ubah mempertahankan status dan id",
                revisi.status() == StatusTugasWeb.SELESAI && revisi.id() == 1);

        periksa("layanan: cari id tidak ada melempar status 404",
                menolak(() -> layanan.cari(99),
                        TugasTidakDitemukanException.class, 404));

        LayananTugasWeb kuota = new LayananTugasWeb(
                new RepositoryTugasWebMemori(), HARI);
        for (int i = 1; i <= LayananTugasWeb.BATAS_TUGAS_AKTIF; i++) {
            kuota.buat(new PermintaanTugas("Tugas kantor " + i, "Dewi", "2026-03-02"));
        }
        periksa("layanan: tugas keenam pemilik sama ditolak status 409",
                menolak(() -> kuota.buat(new PermintaanTugas(
                        "Tugas kantor 6", "Dewi", "2026-03-02")),
                        AturanBisnisException.class, 409));
    }

    /** Tingkat 1c: repository berkas menulis, memuat kembali, dan meneruskan id. */
    private static void ujiRepositoryBerkas(Path berkas) throws IOException {
        RepositoryTugasWebBerkas repository = new RepositoryTugasWebBerkas(berkas);
        repository.simpanBaru(new TugasWeb(0, "Belajar arsitektur berlapis", "Budi",
                StatusTugasWeb.TODO, LocalDate.of(2026, 2, 1)));

        periksa("repository: mutasi menulis berkas", Files.exists(berkas));
        periksa("repository: baris berkas berformat pipa",
                Files.readAllLines(berkas, StandardCharsets.UTF_8).get(0)
                        .equals("1|Belajar arsitektur berlapis|Budi|TODO|2026-02-01"));

        RepositoryTugasWebBerkas termuat = new RepositoryTugasWebBerkas(berkas);
        periksa("repository: berkas dimuat kembali",
                termuat.jumlahTermuat() == 1 && termuat.cari(1).isPresent());
        periksa("repository: id berlanjut dari tertinggi",
                termuat.simpanBaru(new TugasWeb(0, "Riset keamanan aplikasi web",
                        "Dewi", StatusTugasWeb.TODO, LocalDate.of(2026, 1, 25))).id() == 2);
    }

    /** Tingkat 2a: gerbang keamanan menolak permintaan tanpa identitas sah. */
    private static void ujiAutentikasi(HttpClient klien, String dasar)
            throws IOException, InterruptedException {
        HttpResponse<String> tanpa = minta(klien, dasar, null, "GET", "/api/tugas", null);
        periksa("integrasi: tanpa kredensial dijawab 401", tanpa.statusCode() == 401);
        periksa("integrasi: penolakan memuat WWW-Authenticate",
                tanpa.headers().firstValue("WWW-Authenticate").isPresent());

        HttpResponse<String> salah = minta(klien, dasar,
                basic("budi", "sandi-salah"), "GET", "/api/tugas", null);
        periksa("integrasi: sandi salah dijawab 401 dengan body JSON",
                salah.statusCode() == 401 && salah.body().contains("\"status\":401"));
    }

    /** Tingkat 2b: CRUD, validasi, dan otorisasi melalui HTTP sungguhan. */
    private static void ujiCrud(HttpClient klien, String dasar)
            throws IOException, InterruptedException {
        String budi = basic("budi", "sandi-budi");
        String siti = basic("siti", "sandi-siti");

        HttpResponse<String> galat = minta(klien, dasar, budi, "POST", "/api/tugas",
                "{\"judul\":\"ab\",\"pemilik\":\"\",\"tenggat\":\"besok\"}");
        periksa("integrasi: muatan salah dijawab 400 dengan rincian",
                galat.statusCode() == 400 && galat.body().contains("\"galat\":["));

        HttpResponse<String> dibuat = minta(klien, dasar, budi, "POST", "/api/tugas",
                "{\"judul\":\"Belajar arsitektur berlapis\",\"pemilik\":\"Budi\","
                        + "\"tenggat\":\"2026-02-01\"}");
        periksa("integrasi: POST sah dijawab 201 dengan Location",
                dibuat.statusCode() == 201
                        && dibuat.headers().firstValue("Location").orElse("")
                                .equals("/api/tugas/1"));

        minta(klien, dasar, budi, "POST", "/api/tugas",
                "{\"judul\":\"Riset keamanan aplikasi web\",\"pemilik\":\"Dewi\","
                        + "\"tenggat\":\"2026-01-25\"}");

        HttpResponse<String> daftar = minta(klien, dasar, budi, "GET", "/api/tugas", null);
        periksa("integrasi: GET daftar menampilkan dua tugas",
                daftar.statusCode() == 200 && daftar.body().contains("\"id\":2"));

        HttpResponse<String> hilang = minta(klien, dasar, budi, "GET", "/api/tugas/99", null);
        periksa("integrasi: id tidak ada dijawab 404", hilang.statusCode() == 404);

        HttpResponse<String> asing = minta(klien, dasar, budi, "PATCH", "/api/tugas/1", null);
        periksa("integrasi: metode asing dijawab 405", asing.statusCode() == 405);

        HttpResponse<String> terlarang = minta(klien, dasar, budi,
                "DELETE", "/api/tugas/1", null);
        periksa("integrasi: DELETE oleh ANGGOTA dijawab 403",
                terlarang.statusCode() == 403);

        HttpResponse<String> terhapus = minta(klien, dasar, siti,
                "DELETE", "/api/tugas/1", null);
        periksa("integrasi: DELETE oleh ADMIN dijawab 204",
                terhapus.statusCode() == 204);
    }

    /** Tingkat 2c: server baru atas berkas yang sama membuktikan persistensi. */
    private static void ujiPersistensi(HttpClient klien, String dasar)
            throws IOException, InterruptedException {
        String siti = basic("siti", "sandi-siti");

        HttpResponse<String> daftar = minta(klien, dasar, siti, "GET", "/api/tugas", null);
        periksa("persistensi: tugas tersisa termuat setelah restart",
                daftar.statusCode() == 200 && daftar.body().contains("\"id\":2")
                        && !daftar.body().contains("\"id\":1"));

        HttpResponse<String> lanjut = minta(klien, dasar, siti, "POST", "/api/tugas",
                "{\"judul\":\"Menulis dokumentasi API\",\"pemilik\":\"Siti\","
                        + "\"tenggat\":\"2026-01-20\"}");
        periksa("persistensi: id baru berlanjut setelah restart",
                lanjut.statusCode() == 201
                        && lanjut.headers().firstValue("Location").orElse("")
                                .equals("/api/tugas/3"));
    }

    /** Server uji: filter keamanan membungkus router di atas repository berkas. */
    private static HttpServer mulai(Path berkas) throws IOException {
        LayananTugasWeb layanan = new LayananTugasWeb(
                new RepositoryTugasWebBerkas(berkas), HARI);
        HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
        server.createContext("/", new FilterTokenWeb(new RouterWeb(layanan),
                new PenggunaWeb("budi", "sandi-budi", PenggunaWeb.Peran.ANGGOTA),
                new PenggunaWeb("siti", "sandi-siti", PenggunaWeb.Peran.ADMIN)));
        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();
    }

    /** Header Authorization Basic dari pasangan nama dan sandi. */
    private static String basic(String nama, String sandi) {
        byte[] oktet = (nama + ":" + sandi).getBytes(StandardCharsets.UTF_8);
        return "Basic " + Base64.getEncoder().encodeToString(oktet);
    }

    /**
     * Mengirim satu permintaan; 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());
    }

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

    /**
     * Menjalankan perintah; true bila melempar exception bertepatan jenis dan
     * status. Perbandingan getClass() menolak exception salah jenis, dan status
     * dibaca lewat ApiException sehingga angka HTTP ikut diperiksa.
     */
    private static boolean menolak(Perintah perintah,
            Class<? extends ApiException> jenis, int status) {
        try {
            perintah.jalankan();
            return false;
        } catch (Exception e) {
            return e.getClass() == jenis && ((ApiException) e).status() == status;
        }
    }

    /** Antarmuka satu method agar blok uji bisa melempar exception terperiksa. */
    interface Perintah {
        void jalankan() throws Exception;
    }
}
