package org.xbib.logging.ext.test.handlers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;

import org.xbib.logging.ExtHandler;
import org.xbib.logging.ExtLogRecord;
import org.xbib.logging.ext.formatters.PatternFormatter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.xbib.logging.ext.test.AssertingErrorManager;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

public class AbstractHandlerTest {

    private static final Path BASE_LOG_DIR = Path.of(System.getProperty("user.dir") + "/build");

    final static PatternFormatter FORMATTER = new PatternFormatter("%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n");

    private TestInfo testInfo;

    public AbstractHandlerTest() {
    }

    @BeforeEach
    public void setup(final TestInfo testInfo) throws Exception {
        this.testInfo = testInfo;
        deletePath(logDirectory(testInfo));
    }

    @Test
    public void simple() {
        assertTrue(testInfo.getTestMethod().isPresent());
    }

    protected Path resolvePath(final String filename) throws IOException {
        return logDirectory().resolve(filename);
    }

    protected Path logDirectory() throws IOException {
        return logDirectory(testInfo);
    }

    protected Path logDirectory(final TestInfo testInfo) throws IOException {
        assertTrue(testInfo.getTestClass().isPresent());
        assertTrue(testInfo.getTestMethod().isPresent());
        final Path dir =
                BASE_LOG_DIR.resolve(testInfo.getTestClass().get().getSimpleName() + "-" + testInfo.getTestMethod().get().getName());
        if (Files.notExists(dir)) {
            Files.createDirectories(dir);
        }
        return dir;
    }

    private static void deletePath(final Path path) throws IOException {
        if (Files.isDirectory(path)) {
            try (Stream<Path> paths = Files.walk(path)) {
                paths.sorted(Comparator.reverseOrder())
                        .forEach(p -> {
                            try {
                                Files.delete(p);
                            } catch (IOException e) {
                                System.out.printf("Failed to delete path %s%n", p);
                                e.printStackTrace(System.out);
                            }
                        });
            }
        } else {
            Files.delete(path);
        }
    }

    protected static void configureHandlerDefaults(final ExtHandler handler) {
        handler.setAutoFlush(true);
        handler.setFormatter(FORMATTER);
        handler.setErrorManager(AssertingErrorManager.of());
    }

    protected ExtLogRecord createLogRecord(final String msg) {
        return createLogRecord(org.xbib.logging.Level.INFO, msg);
    }

    protected ExtLogRecord createLogRecord(final String format, final Object... args) {
        return createLogRecord(org.xbib.logging.Level.INFO, format, args);
    }

    protected ExtLogRecord createLogRecord(final org.xbib.logging.Level level, final String msg) {
        return new ExtLogRecord(level, msg, getClass().getName());
    }

    protected ExtLogRecord createLogRecord(final org.xbib.logging.Level level, final String format, final Object... args) {
        return new ExtLogRecord(level, String.format(format, args), getClass().getName());
    }

    /**
     * Validates that at least one line of the GZIP'd file contains the expected text.
     *
     * @param path             the path to the GZIP file
     * @param expectedContains the expected text
     *
     * @throws IOException if an error occurs while reading the GZIP file
     */
    static void validateGzipContents(final Path path, final String expectedContains) throws IOException {
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(new GZIPInputStream(Files.newInputStream(path))))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(expectedContains)) {
                    return;
                }
            }
        }
        fail(String.format("GZIP file %s missing contents: %s", path, expectedContains));
    }

    /**
     * Validates that the ZIP file contains the expected file, the expected file is not empty and that the first line
     * contains the expected text.
     *
     * @param path             the path to the zip file
     * @param expectedFileName the name of the file inside the zip file
     * @param expectedContains the expected text
     *
     * @throws IOException if an error occurs reading the zip file
     */
    static void validateZipContents(final Path path, final String expectedFileName, final String expectedContains)
            throws IOException {
        try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + path.toUri().toASCIIString()),
                Collections.singletonMap("create", "true"))) {
            final Path file = zipFs.getPath(zipFs.getSeparator(), expectedFileName);
            assertTrue(Files.exists(file), () -> String.format("Expected file %s not found.", expectedFileName));
            final List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
            assertFalse(lines.isEmpty(),
                    () -> String.format("File %s appears to be empty in zip file %s.", expectedFileName, path));
            assertTrue(lines.getFirst().contains(expectedContains),
                    () -> String.format("ZIP file %s missing contents: %s", path, expectedContains));
        }
    }

    static void compareArchiveContents(final Path archive1, final Path archive2, final String expectedFileName)
            throws IOException {
        Collection<String> lines1 = Collections.emptyList();
        Collection<String> lines2 = Collections.emptyList();

        if (archive1.getFileName().toString().endsWith(".zip")) {
            lines1 = readAllLinesFromZip(archive1, expectedFileName);
            lines2 = readAllLinesFromZip(archive2, expectedFileName);
        } else if (archive1.getFileName().toString().endsWith(".gz")) {
            lines1 = readAllLinesFromGzip(archive1);
            lines2 = readAllLinesFromGzip(archive2);
        } else {
            fail(String.format("Files %s and %s are not archives.", archive1, archive2));
        }

        assertFalse(lines1.isEmpty(), () -> String.format("Archive %s contained no data", archive1));
        assertFalse(lines2.isEmpty(), () -> String.format("Archive %s contained no data", archive2));

        final Collection<String> copy1 = new ArrayList<>(lines1);
        final Collection<String> copy2 = new ArrayList<>(lines2);
        boolean altered = copy1.removeAll(copy2);
        if (copy1.isEmpty()) {
            fail(String.format("The contents of %s and %s are identical and should not be", archive1, archive2));
        } else if (altered) {
            final StringBuilder msg = new StringBuilder(1024)
                    .append("The following contents are in both ")
                    .append(archive1)
                    .append(" and ")
                    .append(archive2);
            // Find the identical lines and report
            for (String line : lines1) {
                if (lines2.contains(line)) {
                    msg.append(System.lineSeparator()).append(line);
                }
            }
            fail(msg.toString());
        }
    }

    private static Collection<String> readAllLinesFromZip(final Path path, final String expectedFileName) throws IOException {
        try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + path.toUri().toASCIIString()),
                Collections.singletonMap("create", "true"))) {
            final Path file = zipFs.getPath(zipFs.getSeparator(), expectedFileName);
            assertTrue(Files.exists(file), () -> String.format("Expected file %s not found.", expectedFileName));
            return Files.readAllLines(file, StandardCharsets.UTF_8);
        }
    }

    private static Collection<String> readAllLinesFromGzip(final Path path) throws IOException {
        final Collection<String> lines = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(new GZIPInputStream(Files.newInputStream(path))))) {
            String line;
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        }
        return lines;
    }
}
