import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 * Harness pengujian berbasis main: setiap pemeriksaan bernama,
 * hasil dicetak PASS/FAIL, dan kegagalan mengubah exit code menjadi 1.
 */
public class UjiKontak {

    private static int gagal = 0;

    public static void main(String[] args) throws Exception {
        ujiValidasi();
        ujiCrud();
        ujiPencarian();
        ujiDaoMemori();
        ujiDaoBerkas();
        ujiBerkasRusak();

        System.out.println();
        System.out.println("Pengujian selesai; jumlah gagal: " + gagal + ".");
        if (gagal > 0) {
            System.exit(1);
        }
    }

    private static void ujiValidasi() throws Exception {
        DaftarKontak daftar = new DaftarKontak();
        periksa("nama kosong ditolak",
                lempar(() -> daftar.tambah("   ", "x@example.com", "0812"),
                        KontakInvalidException.class));
        periksa("email tanpa @ ditolak",
                lempar(() -> daftar.tambah("Rina", "rina.example.com", "0812"),
                        KontakInvalidException.class));
        periksa("email tanpa titik pada domain ditolak",
                lempar(() -> daftar.tambah("Rina", "rina@localhost", "0812"),
                        KontakInvalidException.class));
        periksa("telepon kosong ditolak",
                lempar(() -> daftar.tambah("Rina", "rina@example.com", "  "),
                        KontakInvalidException.class));
        periksa("field memuat karakter | ditolak",
                lempar(() -> daftar.tambah("Budi|Dua", "budi@example.com", "0812"),
                        KontakInvalidException.class));

        DaftarKontak berisi = new DaftarKontak();
        berisi.tambah("Budi", "budi@example.com", "0812-1000-1001");
        periksa("email duplikat ditolak",
                lempar(() -> berisi.tambah("Budi Kedua", "BUDI@example.com", "0812"),
                        KontakInvalidException.class));
    }

    private static void ujiCrud() throws Exception {
        DaftarKontak daftar = new DaftarKontak();
        Kontak budi = daftar.tambah("  Budi  ", "budi@example.com", "0812-1000-1001");
        daftar.tambah("Siti", "siti@example.com", "0812-1000-1002");
        daftar.tambah("Dewi", "dewi@example.com", "0813-2000-2003");

        periksa("tambah mengembalikan id pertama 1", budi.id() == 1);
        periksa("tambah memangkas spasi field",
                budi.nama().equals("Budi"));
        periksa("id bertambah berurutan",
                daftar.semua().get(2).id() == 3);

        Kontak ubah = daftar.ubah(2, "Siti Rahayu", "siti@example.com",
                "0815-3000-3004");
        periksa("ubah mempertahankan id",
                ubah.id() == 2 && daftar.cari(2).orElseThrow().nama().equals("Siti Rahayu"));
        periksa("ubah id tidak ada ditolak",
                lempar(() -> daftar.ubah(9, "X", "x@example.com", "0812"),
                        KontakTidakDitemukanException.class));

        Kontak terhapus = daftar.hapus(1);
        periksa("hapus mengembalikan kontak yang dihapus",
                terhapus.id() == 1 && daftar.jumlah() == 2);
        periksa("hapus kedua kali id sama ditolak",
                lempar(() -> daftar.hapus(1),
                        KontakTidakDitemukanException.class));
        periksa("id tidak dipakai ulang setelah hapus",
                daftar.tambah("Rina", "rina@example.com", "0814-4000-4005").id() == 4);
    }

    private static void ujiPencarian() throws Exception {
        DaftarKontak daftar = new DaftarKontak();
        daftar.tambah("Budi", "budi@example.com", "0812-1000-1001");
        daftar.tambah("Siti", "siti@example.com", "0812-1000-1002");
        daftar.tambah("Dewi", "dewi@example.com", "0813-2000-2003");

        periksa("pencarian mengabaikan huruf besar",
                daftar.cari("SIT").size() == 1
                        && daftar.cari("SIT").get(0).nama().equals("Siti"));
        periksa("pencarian pada telepon menemukan dua kontak",
                daftar.cari("0812").size() == 2);
        periksa("pencarian tanpa hasil mengembalikan daftar kosong",
                daftar.cari("tidak-ada").isEmpty());
        periksa("cari id yang tidak ada mengembalikan kosong",
                daftar.cari(99).isEmpty());
    }

    private static void ujiDaoMemori() throws Exception {
        KontakDaoMemori dao = new KontakDaoMemori();
        dao.tambah(new Kontak(1, "Budi", "budi@example.com", "0812-1000-1001"));
        dao.tambah(new Kontak(2, "Siti", "siti@example.com", "0812-1000-1002"));
        dao.perbarui(new Kontak(2, "Siti Rahayu", "siti@example.com", "0815-3000-3004"));

        periksa("dao memori memuat dua kontak", dao.muatSemua().size() == 2);
        periksa("dao memori menerapkan pembaruan",
                dao.muatSemua().get(1).nama().equals("Siti Rahayu"));
        dao.hapus(1);
        periksa("dao memori menghapus kontak", dao.muatSemua().size() == 1);
    }

    private static void ujiDaoBerkas() throws Exception {
        Path berkas = Path.of("uji-kontak.txt");
        Files.deleteIfExists(berkas);  // mulai kosong agar eksekusi berulang identik
        DaftarKontak daftar = new DaftarKontak(new KontakDaoBerkas(berkas));
        daftar.tambah("Budi", "budi@example.com", "0812-1000-1001");
        daftar.tambah("Siti", "siti@example.com", "0812-1000-1002");
        daftar.tambah("Dewi", "dewi@example.com", "0813-2000-2003");

        periksa("berkas tersimpan", Files.exists(berkas));
        periksa("baris pertama berkas benar",
                Files.readAllLines(berkas, StandardCharsets.UTF_8).get(0)
                        .equals("1|Budi|budi@example.com|0812-1000-1001"));

        DaftarKontak termuat = new DaftarKontak(new KontakDaoBerkas(berkas));
        periksa("daftar baru memuat data dari berkas",
                termuat.jumlah() == 3 && termuat.cari(3).isPresent());

        daftar.ubah(2, "Siti Rahayu", "siti@example.com", "0815-3000-3004");
        daftar.hapus(1);
        DaftarKontak akhir = new DaftarKontak(new KontakDaoBerkas(berkas));
        periksa("ubah dan hapus tercermin pada berkas",
                akhir.jumlah() == 2
                        && akhir.cari(2).orElseThrow().nama().equals("Siti Rahayu"));
        periksa("id berikutnya dilanjutkan dari berkas",
                akhir.tambah("Rina", "rina@example.com", "0814-4000-4005").id() == 4);
    }

    private static void ujiBerkasRusak() throws Exception {
        Path rusak = Path.of("uji-rusak.txt");
        Files.writeString(rusak, "baris tanpa pemisah\n", StandardCharsets.UTF_8);
        periksa("baris rusak memicu PersistensiGagalException",
                lempar(() -> new DaftarKontak(new KontakDaoBerkas(rusak)),
                        PersistensiGagalException.class));
    }

    /** Mencetak hasil satu pemeriksaan dan menghitung kegagalan. */
    private static void periksa(String nama, boolean kondisi) {
        if (kondisi) {
            System.out.println("PASS " + nama);
        } else {
            System.out.println("FAIL " + nama);
            gagal++;
        }
    }

    /** Menjalankan perintah; true bila melempar exception bertipe persis. */
    private static boolean lempar(Perintah perintah,
            Class<? extends Exception> jenis) {
        try {
            perintah.jalankan();
            return false;
        } catch (Exception e) {
            return e.getClass() == jenis;
        }
    }

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