import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;

// Harness pengujian berbasis main: PASS/FAIL tanpa pustaka eksternal.
// Exit code 1 bila ada kegagalan — sinyal bagi proses pemanggil.
public class UjiGudang {

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

    public static void main(String[] args) throws IOException {
        ujiPendaftaran();
        ujiMutasiMenaikkanDanMenurunkan();
        ujiStokTidakCukup();
        ujiBarangTidakDitemukan();
        ujiJumlahTidakPositif();
        ujiMutasiTercatatUrut();
        ujiJurnalSelarasStok();
        ujiPencarian();
        ujiFilterRestok();
        ujiPersistensiBerkas();
        ujiPelolosanCsv();
        System.out.println();
        System.out.println("Ringkasan: " + lulus + " lulus, " + gagal + " gagal");
        if (gagal > 0) {
            System.exit(1);
        }
    }

    /** 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);
        }
    }

    /** Gudang segil berisi satu barang uji. */
    private static Gudang gudangSegil() {
        Gudang gudang = new Gudang(new GudangRepositoryMemori());
        gudang.daftar(new Barang("BRG-001", "Kertas HVS A4", "RIM",
                new BigDecimal("47500"), 0, 10));
        return gudang;
    }

    /** Gudang lima barang dengan mutasi campuran untuk uji pencarian dan filter. */
    private static Gudang gudangLimaBarang() {
        Gudang gudang = new Gudang(new GudangRepositoryMemori());
        gudang.daftar(new Barang("BRG-001", "Kertas HVS A4", "RIM",
                new BigDecimal("47500"), 0, 10));
        gudang.daftar(new Barang("BRG-002", "Tinta Printer Hitam", "BOTOL",
                new BigDecimal("185000"), 0, 8));
        gudang.daftar(new Barang("BRG-003", "Kertas HVS F4", "RIM",
                new BigDecimal("52000"), 0, 8));
        gudang.daftar(new Barang("BRG-004", "Pulpen Gel Hitam 0.5", "BOX",
                new BigDecimal("62000"), 0, 5));
        gudang.daftar(new Barang("BRG-005", "Amplop Cokelat Folio", "PAK",
                new BigDecimal("15500"), 0, 10));
        gudang.barangMasuk("BRG-001", 50, LocalDate.of(2026, 1, 5), "Budi", "penerimaan awal");
        gudang.barangMasuk("BRG-002", 10, LocalDate.of(2026, 1, 5), "Budi", "penerimaan awal");
        gudang.barangMasuk("BRG-003", 20, LocalDate.of(2026, 1, 6), "Dewi", "penerimaan awal");
        gudang.barangMasuk("BRG-004", 12, LocalDate.of(2026, 1, 6), "Dewi", "penerimaan awal");
        gudang.barangMasuk("BRG-005", 30, LocalDate.of(2026, 1, 7), "Dewi", "penerimaan awal");
        gudang.barangKeluar("BRG-002", 8, LocalDate.of(2026, 1, 20), "Siti",
                "permintaan divisi pemasaran");
        gudang.barangKeluar("BRG-003", 17, LocalDate.of(2026, 1, 22), "Siti",
                "permintaan divisi keuangan");
        return gudang;
    }

    private static void ujiPendaftaran() {
        Gudang gudang = gudangSegil();
        cek("barang terdaftar ditemukan lewat kode",
                gudang.cariBarang("BRG-001").nama().equals("Kertas HVS A4"));
        try {
            gudang.daftar(new Barang("BRG-001", "Kertas HVS A4", "RIM",
                    new BigDecimal("47500"), 0, 10));
            cek("kode ganda ditolak", false);
        } catch (GudangException e) {
            cek("kode ganda ditolak", e.getMessage().contains("sudah terdaftar"));
        }
    }

    private static void ujiMutasiMenaikkanDanMenurunkan() {
        Gudang gudang = gudangSegil();
        gudang.barangMasuk("BRG-001", 50, LocalDate.of(2026, 1, 5), "Budi", "penerimaan awal");
        gudang.barangKeluar("BRG-001", 20, LocalDate.of(2026, 1, 8), "Siti", "permintaan");
        cek("masuk 50 keluar 20 menyisakan stok 30",
                gudang.cariBarang("BRG-001").stok() == 30);
    }

    /** Aturan inti: stok tidak boleh minus, penolakan tidak meninggalkan jejak. */
    private static void ujiStokTidakCukup() {
        Gudang gudang = gudangSegil();
        gudang.barangMasuk("BRG-001", 10, LocalDate.of(2026, 1, 5), "Budi", "penerimaan awal");
        int stokSebelum = gudang.cariBarang("BRG-001").stok();
        int mutasiSebelum = gudang.riwayat().size();
        try {
            gudang.barangKeluar("BRG-001", 11, LocalDate.of(2026, 1, 6), "Siti", "permintaan");
            cek("keluar melebihi stok melempar StokTidakCukupException", false);
        } catch (StokTidakCukupException e) {
            cek("keluar melebihi stok melempar StokTidakCukupException",
                    e.getMessage().contains("tersisa 10"));
        }
        cek("stok tidak berubah setelah penolakan",
                gudang.cariBarang("BRG-001").stok() == stokSebelum);
        cek("tidak ada jurnal untuk transaksi yang ditolak",
                gudang.riwayat().size() == mutasiSebelum);
    }

    private static void ujiBarangTidakDitemukan() {
        Gudang gudang = gudangSegil();
        try {
            gudang.barangMasuk("BRG-009", 5, LocalDate.of(2026, 1, 5), "Budi", "uji");
            cek("kode tak dikenal melempar BarangTidakDitemukanException", false);
        } catch (BarangTidakDitemukanException e) {
            cek("kode tak dikenal melempar BarangTidakDitemukanException",
                    e.getMessage().contains("BRG-009"));
        }
    }

    private static void ujiJumlahTidakPositif() {
        Gudang gudang = gudangSegil();
        gudang.barangMasuk("BRG-001", 10, LocalDate.of(2026, 1, 5), "Budi", "penerimaan awal");
        try {
            gudang.barangMasuk("BRG-001", 0, LocalDate.of(2026, 1, 6), "Budi", "uji");
            cek("jumlah nol ditolak", false);
        } catch (GudangException e) {
            cek("jumlah nol ditolak", e.getMessage().contains("positif"));
        }
    }

    private static void ujiMutasiTercatatUrut() {
        Gudang gudang = gudangSegil();
        gudang.barangMasuk("BRG-001", 10, LocalDate.of(2026, 1, 5), "Budi", "pertama");
        gudang.barangMasuk("BRG-001", 5, LocalDate.of(2026, 1, 6), "Siti", "kedua");
        gudang.barangKeluar("BRG-001", 3, LocalDate.of(2026, 1, 7), "Dewi", "ketiga");
        List<MutasiStok> riwayat = gudang.riwayat();
        cek("riwayat tercatat urut dengan id 1, 2, 3",
                riwayat.get(0).id() == 1 && riwayat.get(1).id() == 2
                        && riwayat.get(2).id() == 3);
        cek("urutan riwayat mengikuti urutan pemanggilan",
                riwayat.get(2).catatan().equals("ketiga"));
    }

    /** Invarian Bagian 1: stok selalu sama dengan penjumlahan seluruh jurnal. */
    private static void ujiJurnalSelarasStok() {
        Gudang gudang = gudangSegil();
        gudang.barangMasuk("BRG-001", 50, LocalDate.of(2026, 1, 5), "Budi", "awal");
        gudang.barangKeluar("BRG-001", 20, LocalDate.of(2026, 1, 8), "Siti", "permintaan");
        gudang.barangMasuk("BRG-001", 7, LocalDate.of(2026, 1, 10), "Dewi", "koreksi");
        int dariJurnal = 0;
        for (MutasiStok m : gudang.riwayat()) {
            dariJurnal += m.jenis().tanda() * m.jumlah();
        }
        cek("stok selalu sama dengan penjumlahan jurnal",
                gudang.cariBarang("BRG-001").stok() == dariJurnal);
    }

    private static void ujiPencarian() {
        Gudang gudang = gudangLimaBarang();
        cek("pencarian huruf kecil menemukan 2 kertas",
                gudang.cariKataKunci("kertas").size() == 2);
        cek("pencarian huruf besar memberi hasil identik",
                gudang.cariKataKunci("KERTAS").equals(gudang.cariKataKunci("kertas")));
        cek("kata tanpa padanan menghasilkan daftar kosong",
                gudang.cariKataKunci("kursi").isEmpty());
    }

    private static void ujiFilterRestok() {
        Gudang gudang = gudangLimaBarang();
        List<Barang> menipis = gudang.barangPerluRestok();
        cek("filter restok menemukan 2 barang", menipis.size() == 2);
        cek("filter restok terurut naik berdasarkan kode",
                menipis.get(0).kode().equals("BRG-002")
                        && menipis.get(1).kode().equals("BRG-003"));
    }

    private static void ujiPersistensiBerkas() throws IOException {
        Path berkas = Path.of("uji-gudang.txt");
        Files.deleteIfExists(berkas);   // mulai bersih agar uji dapat diulang

        Gudang pertama = new Gudang(new GudangRepositoryBerkas(berkas));
        pertama.daftar(new Barang("BRG-001", "Kertas HVS A4", "RIM",
                new BigDecimal("47500"), 0, 10));
        pertama.barangMasuk("BRG-001", 50, LocalDate.of(2026, 1, 5), "Budi", "penerimaan awal");
        pertama.barangKeluar("BRG-001", 20, LocalDate.of(2026, 1, 8), "Siti", "permintaan");

        Gudang kedua = new Gudang(new GudangRepositoryBerkas(berkas));
        cek("berkas memuat kembali barang yang sama",
                kedua.cariBarang("BRG-001").equals(pertama.cariBarang("BRG-001")));
        cek("berkas memuat kembali seluruh jurnal",
                kedua.riwayat().equals(pertama.riwayat()));
        MutasiStok lanjutan = kedua.barangMasuk("BRG-001", 5,
                LocalDate.of(2026, 1, 25), "Dewi", "pembelian rutin");
        cek("id mutasi berlanjut setelah dibuka ulang", lanjutan.id() == 3);
    }

    private static void ujiPelolosanCsv() throws IOException {
        cek("nilai polos tidak dibungkus",
                LaporanCsv.csv("Kertas HVS A4").equals("Kertas HVS A4"));
        cek("nilai berkoma dibungkus kutip ganda",
                LaporanCsv.csv("a,b").equals("\"a,b\""));
        cek("kutip asli digandakan di dalam pembungkus",
                LaporanCsv.csv("kata \"x\"").equals("\"kata \"\"x\"\"\""));

        Gudang gudang = gudangSegil();
        gudang.barangMasuk("BRG-001", 10, LocalDate.of(2026, 1, 5), "Budi", "penerimaan awal");
        gudang.barangKeluar("BRG-001", 2, LocalDate.of(2026, 1, 6), "Siti",
                "koreksi, \"stok opname\"");
        Path berkas = Path.of("uji-mutasi.csv");
        LaporanCsv.eksporMutasi(gudang.riwayat(), berkas);
        List<String> isi = Files.readAllLines(berkas, StandardCharsets.UTF_8);
        cek("laporan mutasi berisi header dan 2 baris", isi.size() == 3);
        cek("baris terakhir terpelolos dengan benar",
                isi.get(2).endsWith(",\"koreksi, \"\"stok opname\"\"\""));
    }
}
