package de.duehl.basics.test;

import java.util.ArrayList;
import java.util.List;

/*
 * Copyright 2023 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import de.duehl.basics.io.Charset;
import de.duehl.basics.io.FileHelper;
import de.duehl.basics.io.FineFileWriter;
import de.duehl.basics.io.Writer;
import de.duehl.basics.text.Lines;
import de.duehl.basics.text.NumberString;
import de.duehl.basics.text.Text;

/**
 * Dieses Klasse erstellt Unit-Tests in Dateien und sorgt dafür, dass diese nicht zu groß werden.
 * Ist das der Fall, wird eine neue aufgemacht.
 *
 * @version 1.01     2023-06-07
 * @author Christian Dühl
 */

public class UnitTestCreator {

    /** Platzhalter im Quellcode, an dem der Klassennamen eingesetzt wird. */
    public static final String CLASS_NAME_PLACEHOLDER = "###CLASS_NAME###";

    /** Platzhalter im Quellcode, an dem der letzte Teil des Pakets eingesetzt wird. */
    public static final String LAST_PACKAGE_PART_PLACEHOLDER = "###PACKAGE_PART###";

    /** Maximale Anzahl an Zeilen pro Test-Datei. */
    private static final int MAXIMAL_NUMBER_OF_LINES_PER_TESTFILE = 10_000;

    /** Das Beschreibungskürzel für Datei- und Klassennamen. */
    private final String description;

    /** Das vorn kleingeschriebene Beschreibungskürzel für Datei- und Klassennamen. */
    private final String descriptionLoweredAtFront;

    /**
     * Beschreibungskürzel das angibt, ob es sich um gute Tests oder solche, an denen noch etwas
     * zu tun ist, handelt. In der Regel sollte man "Good" oder "Todo" übergeben.
     */
    private final String goodOrTodoDescription;

    /**
     * Kopf der Java-Test-Datei, es wird erwartet, dass für den Klassennamen der Platzhalter
     * "###CLASS_NAME###" eingetragen ist.
     */
    private final Lines javaTestFileHead;

    /** Ende der Java-Test-Datei. */
    private final Lines javaTestFileTail;

    /** Verzeichnis für die Tests. */
    private String testDirectory;

    /** Gibt an, ob wir gerade eine Datei offen haben, in der Test-Methoden geschrieben werden. */
    private boolean isTestFileOpen;

    /** Gibt an, wieviele Zeilen die geöffnete Datei bereits enthält. */
    private int lineCountInOpenTestFile;

    /** Objekt, dass die aktuelle Testdatei herausschreibt. */
    private Writer writer;

    /** Anzahl der geschriebenen Test-Dateien. */
    private int numberOfTestFiles;

    /** Anzahl Testmethoden in allen geschriebenen Dateien. */
    private int numberOfTestMethodsInAllFiles;

    /** Der Prefix im Paket vor den Klassennamen. */
    private String packagePartPrefix;

    /**
     * Gibt an, ob das Testverzeichnis bereits passend modifiziert wurde. Dazu werden ggf. der
     * Prefix und die vorn kleingeschriebene Beschreibung (descriptionLoweredAtFront) angehängt.
     *
     * Dies soll nur einmal zu Beginn erfolgen, da manchmal setPackagePartPrefix() aufgerufen wird,
     * und manchmal nicht, bleibt nur, zu Beginn von writeTest() zu prüfen, ob diese Modifikation
     * notwendig ist.
     */
    private boolean testDirectoryIsModified;

    /**
     * Konstruktor.
     *
     * @param description
     *            Beschreibungskürzel für Datei- und Klassennamen. Sollte mit einem großen
     *            Buchstaben anfangen.
     * @param goodOrTotoDescription
     *            Beschreibungskürzel das angibt, ob es sich um gute Tests oder solche, an denen
     *            noch etwas zu tun ist, handelt. In der Regel sollte man "Good" oder "Todo"
     *            übergeben.
     * @param javaTestFileHead
     *            Kopf der Java-Test-Datei, es wird erwartet, dass für den Klassennamen der
     *            Platzhalter "###CLASS_NAME###" eingetragen ist.
     * @param javaTestFileTail
     *            Ende der Java-Test-Datei.
     * @param testDirectory
     *            Verzeichnis für die Tests, in diesem wird ein Unterverzeichnis basierend auf dem
     *            Kürzel angelegt, in dem die Testdateien erzeugt werden.
     */
    public UnitTestCreator(String description, String goodOrTotoDescription, Lines javaTestFileHead,
            Lines javaTestFileTail, String testDirectory) {
        this.description = description;
        this.goodOrTodoDescription = goodOrTotoDescription;
        this.javaTestFileHead = javaTestFileHead;
        this.javaTestFileTail = javaTestFileTail;
        this.testDirectory = testDirectory;

        descriptionLoweredAtFront = Text.firstCharToLowerCase(description);

        initNotOpenTestFile();
        numberOfTestFiles = 0;
        numberOfTestMethodsInAllFiles = 0;

        packagePartPrefix = "";
        testDirectoryIsModified = false;
    }

    /**
     * Setzt den Prefix im Paket vor den Klassennamen.
     *
     * Dieser muss auf "." enden.
     *
     * Für jeden an Punkten aufgeteilten Begriff darin wird auch das Verzeichnis angepasst, in das
     * der Test geschrieben wird. Entsprechend notwendige Verzeichnisse werden im Zweifelsfall
     * angelegt.
     */
    public void setPackagePartPrefix(String packagePartPrefix) {
        if (!packagePartPrefix.endsWith(".")) {
            throw new RuntimeException("Das nichtleere packagePartPrefix '"
                    + packagePartPrefix + "' endet nicht auf einen Punkt.");
        }
        this.packagePartPrefix = packagePartPrefix;
    }

    /** Übergibt den Quellcode einer Testmethode. */
    public void writeTest(Lines lines) {
        modifyTestDirectoryIfNecessary();

        if (isTestFileOpen) {
            int lengthWithNewTest = lineCountInOpenTestFile + lines.size() + javaTestFileTail.size();
            if (lengthWithNewTest > MAXIMAL_NUMBER_OF_LINES_PER_TESTFILE) {
                closeActualTestFile();
                createNewTestFile();
            }
        }
        else {
            createNewTestFile();
        }
        writeTestInOpenTestFile(lines);
    }

    private void modifyTestDirectoryIfNecessary() {
        if (!testDirectoryIsModified) {
            modifyTestDirectory();
            testDirectoryIsModified = true;
            Text.say("Erzeuge Tests im Verzeichnis " + testDirectory);
        }
    }

    private void modifyTestDirectory() {
        FileHelper.createDirectoryIfNotExists(testDirectory);

        List<String> bareSubdirectories = new ArrayList<>();

        if (!packagePartPrefix.isEmpty()) {
            String prefix = packagePartPrefix.substring(0, packagePartPrefix.length() - 1);
            List<String> parts = Text.splitByDot(prefix);
            bareSubdirectories.addAll(parts);
        }

        bareSubdirectories.add(descriptionLoweredAtFront);

        for (String bareSubdirectory : bareSubdirectories) {
            testDirectory = FileHelper.concatPathes(testDirectory, bareSubdirectory);
            FileHelper.createDirectoryIfNotExists(this.testDirectory);
        }
    }

    private void createNewTestFile() {
        ++numberOfTestFiles;
        String className = goodOrTodoDescription + "Test" + description
                + NumberString.addLeadingZeroes(numberOfTestFiles, 5);
        String bareFileName = className + ".java";
        String filename = FileHelper.concatPathes(testDirectory, bareFileName);

        String lastPackagePart = packagePartPrefix + descriptionLoweredAtFront;

        writer = new FineFileWriter(filename, Charset.UTF_8);
        isTestFileOpen = true;
        lineCountInOpenTestFile = 0;
        for (String headLine : javaTestFileHead) {
            String replacedLine = headLine;
            replacedLine = replacedLine.replace(CLASS_NAME_PLACEHOLDER, className);
            replacedLine = replacedLine.replace(LAST_PACKAGE_PART_PLACEHOLDER, lastPackagePart);
            writer.writeln(replacedLine);
            ++lineCountInOpenTestFile;
        }
        writer.writeln();
    }

    private void closeActualTestFile() {
        writer.writeAllLines(javaTestFileTail);
        writer.close();
        initNotOpenTestFile();
    }

    private void initNotOpenTestFile() {
        isTestFileOpen = false;
        lineCountInOpenTestFile = -1;
    }

    private void writeTestInOpenTestFile(Lines lines) {
        writer.writeAllLines(lines);
        lineCountInOpenTestFile += lines.size();
        ++numberOfTestMethodsInAllFiles;
    }

    /**
     * Beendet das Rausschreiben der Testdateien, nachdem der letzte Quellcode einer Testmethode
     * übergeben worden ist.
     */
    public void close() {
        if (isTestFileOpen) {
            closeActualTestFile();
        }
    }

    /** Getter für die Anzahl der geschriebenen Test-Dateien. */
    public int getNumberOfTestFiles() {
        return numberOfTestFiles;
    }

    /** Getter für die Anzahl Testmethoden in allen geschriebenen Dateien. */
    public int getNumberOfTestMethodsInAllFiles() {
        return numberOfTestMethodsInAllFiles;
    }

}
