package evaluation;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;

public class Correcteur {
    private static double note = 0.0;
    private static double total = 0.0;
    private static final List<String[]> lignes = new ArrayList<>();

    private record TestCase(String id, String titre, double points, String[] sources, String code) {}

    public static void main(String[] args) throws Exception {
        List<TestCase> tests = new ArrayList<>();

        tests.add(new TestCase("1.1", "Plat : constructeur, getters, description", 1.0,
            new String[]{"src/ex1_modele/Plat.java", "src/ex1_modele/PlatChaud.java"},
            """
            import ex1_modele.*;
            public class TestQ {
                public static void main(String[] args) {
                    Plat p = new PlatChaud("PIZZA", "Pizza reine", 12.5);
                    check("PIZZA".equals(p.getCode()), "getCode attendu PIZZA");
                    check("Pizza reine".equals(p.getNom()), "getNom attendu Pizza reine");
                    check(Math.abs(p.getPrix() - 12.5) < 0.0001, "getPrix attendu 12.5");
                    check(p.description().contains("PIZZA"), "description doit contenir le code");
                    check(p.description().contains("Pizza reine"), "description doit contenir le nom");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("1.2", "Heritage, protected et categories", 1.5,
            new String[]{"src/ex1_modele/Plat.java", "src/ex1_modele/PlatChaud.java", "src/ex1_modele/Dessert.java", "src/ex1_modele/Boisson.java"},
            """
            import ex1_modele.*;
            public class TestQ {
                public static void main(String[] args) {
                    check("chaud".equals(new PlatChaud("P", "Pizza", 10).getCategorie()), "PlatChaud -> chaud");
                    check("dessert".equals(new Dessert("D", "Tiramisu", 6).getCategorie()), "Dessert -> dessert");
                    check("boisson".equals(new Boisson("B", "Eau", 2).getCategorie()), "Boisson -> boisson");
                    check(new PlatChaud("X", "Test", 9).prixAvecFraisService() > 9.0, "methode fille utilisant le prix herite/protected");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("1.3a", "equals : deux plats meme code sont egaux", 1.0,
            new String[]{"src/ex1_modele/Plat.java", "src/ex1_modele/PlatChaud.java"},
            """
            import ex1_modele.*;
            public class TestQ {
                public static void main(String[] args) {
                    Plat p1 = new PlatChaud("PIZZA", "Pizza reine", 12);
                    Plat p2 = new PlatChaud("PIZZA", "Pizza fromage", 10);
                    check(p1.equals(p2), "deux plats avec meme code doivent etre egaux");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("1.3b", "hashCode : HashSet refuse les doublons", 1.0,
            new String[]{"src/ex1_modele/Plat.java", "src/ex1_modele/PlatChaud.java"},
            """
            import ex1_modele.*;
            import java.util.HashSet;
            public class TestQ {
                public static void main(String[] args) {
                    Plat p1 = new PlatChaud("PIZZA", "Pizza reine", 12);
                    Plat p2 = new PlatChaud("PIZZA", "Pizza fromage", 10);
                    HashSet<Plat> set = new HashSet<>();
                    set.add(p1);
                    set.add(p2);
                    check(set.size() == 1, "HashSet attendu : 1 element");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("1.4", "Factory : creer chaud, dessert, boisson", 1.5,
            new String[]{"src/ex1_modele/Plat.java", "src/ex1_modele/PlatChaud.java", "src/ex1_modele/Dessert.java", "src/ex1_modele/Boisson.java", "src/ex1_modele/PlatFactory.java"},
            """
            import ex1_modele.*;
            public class TestQ {
                public static void main(String[] args) {
                    check(PlatFactory.creer("chaud", "P", "Pizza", 10) instanceof PlatChaud, "chaud -> PlatChaud");
                    check(PlatFactory.creer("dessert", "D", "Gateau", 5) instanceof Dessert, "dessert -> Dessert");
                    check(PlatFactory.creer("boisson", "B", "Eau", 2) instanceof Boisson, "boisson -> Boisson");
                    check(PlatFactory.creer("inconnu", "X", "X", 1) == null, "type inconnu -> null");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("1.5a", "Restaurant : ArrayList ajouter et compter", 0.5,
            new String[]{"src/ex1_modele/Plat.java", "src/ex1_modele/PlatChaud.java", "src/ex1_modele/Restaurant.java"},
            """
            import ex1_modele.*;
            public class TestQ {
                public static void main(String[] args) {
                    Restaurant r = new Restaurant("R1", "Chez Java");
                    r.ajouterPlat(new PlatChaud("P1", "Pizza", 10));
                    r.ajouterPlat(new PlatChaud("P2", "Pasta", 11));
                    check(r.nombrePlats() == 2, "nombrePlats attendu : 2");
                    check(r.getMenu().size() == 2, "getMenu doit contenir 2 plats");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("1.5b", "Restaurant : Iterator supprimer plats trop chers", 0.5,
            new String[]{"src/ex1_modele/Plat.java", "src/ex1_modele/PlatChaud.java", "src/ex1_modele/Restaurant.java"},
            """
            import ex1_modele.*;
            public class TestQ {
                public static void main(String[] args) {
                    Restaurant r = new Restaurant("R1", "Chez Java");
                    r.ajouterPlat(new PlatChaud("P1", "Pizza", 12));
                    r.ajouterPlat(new PlatChaud("P2", "Menu premium", 30));
                    r.supprimerPlatsTropChers(20);
                    check(r.nombrePlats() == 1, "il doit rester 1 plat");
                    check("P1".equals(r.getMenu().get(0).getCode()), "le plat P1 doit rester");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("2.1a", "Commande : HashMap creer une nouvelle cle", 1.0,
            new String[]{"src/ex2_commandes/Commande.java", "src/ex2_commandes/CommandeVideException.java", "src/ex2_commandes/StockInsuffisantException.java"},
            """
            import ex2_commandes.*;
            public class TestQ {
                public static void main(String[] args) {
                    Commande c = new Commande("C1");
                    c.ajouter("PIZZA");
                    check(c.quantite("PIZZA") == 1, "PIZZA doit avoir quantite 1");
                    check(c.quantite("INCONNU") == 0, "INCONNU doit avoir quantite 0");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("2.1b", "Commande : HashMap incrementer une cle existante", 1.0,
            new String[]{"src/ex2_commandes/Commande.java", "src/ex2_commandes/CommandeVideException.java", "src/ex2_commandes/StockInsuffisantException.java"},
            """
            import ex2_commandes.*;
            public class TestQ {
                public static void main(String[] args) {
                    Commande c = new Commande("C1");
                    c.ajouter("PIZZA");
                    c.ajouter("PIZZA");
                    c.ajouter("COCA");
                    check(c.quantite("PIZZA") == 2, "PIZZA doit avoir quantite 2");
                    check(c.quantite("COCA") == 1, "COCA doit avoir quantite 1");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("2.2", "Commande : calculer le total", 1.5,
            new String[]{"src/ex2_commandes/Commande.java", "src/ex2_commandes/CommandeVideException.java", "src/ex2_commandes/StockInsuffisantException.java"},
            """
            import ex2_commandes.*;
            import java.util.*;
            public class TestQ {
                public static void main(String[] args) {
                    Commande c = new Commande("C1");
                    c.ajouter("PIZZA");
                    c.ajouter("PIZZA");
                    c.ajouter("COCA");
                    Map<String, Double> prix = new HashMap<>();
                    prix.put("PIZZA", 12.0);
                    prix.put("COCA", 3.0);
                    check(Math.abs(c.total(prix) - 27.0) < 0.0001, "total attendu : 27.0");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("2.3", "Exception : commande vide", 1.0,
            new String[]{"src/ex2_commandes/Commande.java", "src/ex2_commandes/CommandeVideException.java", "src/ex2_commandes/StockInsuffisantException.java"},
            """
            import ex2_commandes.*;
            public class TestQ {
                public static void main(String[] args) {
                    boolean levee = false;
                    try { new Commande("C1").valider(); }
                    catch (CommandeVideException e) { levee = true; }
                    check(levee, "CommandeVideException doit etre levee");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("2.4", "Exception : stock insuffisant", 1.5,
            new String[]{"src/ex2_commandes/Commande.java", "src/ex2_commandes/CommandeVideException.java", "src/ex2_commandes/StockInsuffisantException.java"},
            """
            import ex2_commandes.*;
            import java.util.*;
            public class TestQ {
                public static void main(String[] args) {
                    Commande c = new Commande("C1");
                    c.ajouter("PIZZA");
                    c.ajouter("PIZZA");
                    c.ajouter("PIZZA");
                    Map<String, Integer> stock = new HashMap<>();
                    stock.put("PIZZA", 2);
                    boolean levee = false;
                    try { c.verifierStock(stock); }
                    catch (StockInsuffisantException e) { levee = true; }
                    check(levee, "StockInsuffisantException doit etre levee");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("2.5", "Singleton : JournalCommandes", 1.0,
            new String[]{"src/ex2_commandes/JournalCommandes.java"},
            """
            import ex2_commandes.*;
            public class TestQ {
                public static void main(String[] args) {
                    JournalCommandes j1 = JournalCommandes.getInstance();
                    JournalCommandes j2 = JournalCommandes.getInstance();
                    check(j1 != null, "getInstance ne doit pas retourner null");
                    check(j1 == j2, "j1 et j2 doivent etre le meme objet");
                    j1.ajouter("test");
                    check(j2.getMessages().contains("test"), "message ajoute via j1 visible via j2");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("3.1", "Strategy : calculs rapide et economique", 1.0,
            new String[]{"src/ex3_livraison/StrategieLivraison.java", "src/ex3_livraison/LivraisonRapide.java", "src/ex3_livraison/LivraisonEconomique.java"},
            """
            import ex3_livraison.*;
            public class TestQ {
                public static void main(String[] args) {
                    check(Math.abs(new LivraisonRapide().calculerFrais(10) - 20.0) < 0.0001, "rapide attendu 20.0");
                    check(Math.abs(new LivraisonEconomique().calculerFrais(10) - 10.0) < 0.0001, "economique attendu 10.0");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("3.2", "Strategy : Livraison utilise une strategie interchangeable", 1.0,
            new String[]{"src/ex3_livraison/StrategieLivraison.java", "src/ex3_livraison/LivraisonRapide.java", "src/ex3_livraison/LivraisonEconomique.java", "src/ex3_livraison/Livraison.java"},
            """
            import ex3_livraison.*;
            public class TestQ {
                public static void main(String[] args) {
                    Livraison l = new Livraison(10, new LivraisonRapide());
                    check(Math.abs(l.calculerFrais() - 20.0) < 0.0001, "rapide attendu 20.0");
                    l.setStrategie(new LivraisonEconomique());
                    check(Math.abs(l.calculerFrais() - 10.0) < 0.0001, "apres changement attendu 10.0");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("3.3", "Observer : notification recue par SuiviClient", 1.5,
            new String[]{"src/ex3_livraison/ObservateurCommande.java", "src/ex3_livraison/CommandeObservable.java", "src/ex3_livraison/SuiviClient.java"},
            """
            import ex3_livraison.*;
            public class TestQ {
                public static void main(String[] args) {
                    CommandeObservable c = new CommandeObservable("C1");
                    SuiviClient suivi = new SuiviClient();
                    c.ajouterObservateur(suivi);
                    c.changerEtat("EN_LIVRAISON");
                    check("EN_LIVRAISON".equals(suivi.getDernierMessage()), "client doit recevoir EN_LIVRAISON");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("3.4", "Adapter : ancien livreur adapte au nouveau systeme", 1.0,
            new String[]{"src/ex3_livraison/Livreur.java", "src/ex3_livraison/AncienLivreur.java", "src/ex3_livraison/LivreurAdapter.java"},
            """
            import ex3_livraison.*;
            public class TestQ {
                public static void main(String[] args) {
                    AncienLivreur ancien = new AncienLivreur("OLD-42");
                    Livreur l = new LivreurAdapter(ancien);
                    check("OLD-42".equals(l.getIdentifiant()), "identifiant attendu OLD-42");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("3.5a", "Streams : filter commandes livrees", 0.5,
            new String[]{"src/ex3_livraison/CommandeInfo.java", "src/ex3_livraison/AnalyseCommandes.java"},
            """
            import ex3_livraison.*;
            import java.util.*;
            public class TestQ {
                public static void main(String[] args) {
                    List<CommandeInfo> list = List.of(new CommandeInfo("A", "LIVREE", 10), new CommandeInfo("B", "EN_COURS", 20), new CommandeInfo("C", "LIVREE", 30));
                    check(AnalyseCommandes.commandesLivrees(list).size() == 2, "2 commandes livrees attendues");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("3.5b", "Streams : map totaux", 0.5,
            new String[]{"src/ex3_livraison/CommandeInfo.java", "src/ex3_livraison/AnalyseCommandes.java"},
            """
            import ex3_livraison.*;
            import java.util.*;
            public class TestQ {
                public static void main(String[] args) {
                    List<CommandeInfo> list = List.of(new CommandeInfo("A", "LIVREE", 10), new CommandeInfo("B", "EN_COURS", 20));
                    List<Double> totaux = AnalyseCommandes.totaux(list);
                    check(totaux.size() == 2, "2 totaux attendus");
                    check(Math.abs(totaux.get(0) - 10.0) < 0.0001, "premier total attendu 10");
                    check(Math.abs(totaux.get(1) - 20.0) < 0.0001, "deuxieme total attendu 20");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        tests.add(new TestCase("3.5c", "Streams : sorted par total decroissant", 0.5,
            new String[]{"src/ex3_livraison/CommandeInfo.java", "src/ex3_livraison/AnalyseCommandes.java"},
            """
            import ex3_livraison.*;
            import java.util.*;
            public class TestQ {
                public static void main(String[] args) {
                    List<CommandeInfo> list = List.of(new CommandeInfo("A", "LIVREE", 10), new CommandeInfo("B", "EN_COURS", 30), new CommandeInfo("C", "LIVREE", 20));
                    List<CommandeInfo> triees = AnalyseCommandes.trierParTotalDecroissant(list);
                    check("B".equals(triees.get(0).getCode()), "B doit etre en premier");
                    check("C".equals(triees.get(1).getCode()), "C doit etre en deuxieme");
                    check("A".equals(triees.get(2).getCode()), "A doit etre en troisieme");
                }
                static void check(boolean ok, String msg) { if(!ok) throw new AssertionError(msg); }
            }
            """));

        for (TestCase t : tests) executerTest(t);
        afficherTableau();
    }

    private static void executerTest(TestCase test) throws Exception {
        total += test.points();
        Path base = Paths.get(".autograde", test.id().replace(".", "_"));
        Path bin = base.resolve("bin");
        Path gen = base.resolve("generated");
        deleteDirectory(base);
        Files.createDirectories(bin);
        Files.createDirectories(gen);
        Path testFile = gen.resolve("TestQ.java");
        Files.writeString(testFile, test.code(), StandardCharsets.UTF_8);

        List<String> args = new ArrayList<>();
        args.add("-encoding");
        args.add("UTF-8");
        args.add("-d");
        args.add(bin.toString());
        for (String s : test.sources()) args.add(s);
        args.add(testFile.toString());

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            lignes.add(new String[]{test.id(), test.titre(), "0", format(test.points()), "ECHEC: JDK requis"});
            return;
        }

        ByteArrayOutputStream err = new ByteArrayOutputStream();
        int res = compiler.run(null, null, err, args.toArray(new String[0]));
        if (res != 0) {
            lignes.add(new String[]{test.id(), test.titre(), "0", format(test.points()), "ECHEC COMPILATION"});
            return;
        }

        ProcessBuilder pb = new ProcessBuilder("java", "-cp", bin.toString(), "TestQ");
        pb.redirectErrorStream(true);
        Process p = pb.start();
        String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
        boolean finished = p.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
        if (!finished) {
            p.destroyForcibly();
            lignes.add(new String[]{test.id(), test.titre(), "0", format(test.points()), "ECHEC: timeout"});
            return;
        }
        if (p.exitValue() == 0) {
            note += test.points();
            lignes.add(new String[]{test.id(), test.titre(), format(test.points()), format(test.points()), "OK"});
        } else {
            lignes.add(new String[]{test.id(), test.titre(), "0", format(test.points()), "ECHEC TEST"});
        }
    }

    private static void afficherTableau() {
        System.out.println("+--------+--------------------------------------------------------------+----------+----------+------------------+");
        System.out.println("| Test   | Competence                                                   | Obtenu   | Attendu  | Resultat         |");
        System.out.println("+--------+--------------------------------------------------------------+----------+----------+------------------+");
        for (String[] l : lignes) {
            System.out.printf("| %-6s | %-60s | %-8s | %-8s | %-16s |%n", l[0], couper(l[1], 60), l[2], l[3], couper(l[4], 16));
        }
        System.out.println("+--------+--------------------------------------------------------------+----------+----------+------------------+");
        System.out.println("NOTE FINALE : " + format(note) + " / " + format(total));
    }

    private static String format(double d) {
        if (Math.abs(d - Math.round(d)) < 0.0001) return String.valueOf((int)Math.round(d));
        return String.format(java.util.Locale.US, "%.1f", d);
    }

    private static String couper(String s, int n) {
        if (s == null || s.length() <= n) return s == null ? "" : s;
        return s.substring(0, n - 3) + "...";
    }

    private static void deleteDirectory(Path path) throws IOException {
        if (!Files.exists(path)) return;
        Files.walk(path).sorted(Comparator.reverseOrder()).forEach(p -> {
            try { Files.deleteIfExists(p); } catch (IOException ignored) {}
        });
    }
}
