package org.micord.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class DownloadServiceTest {

    @InjectMocks
    private DownloadService downloadService;

    @TempDir
    Path tempDir;

    private static final int MAX_ROWS_PER_CSV = 600000;

    @BeforeEach
    void setUp() {
        downloadService = new DownloadService();
    }

    @Test
    void testSingleCsvFileWhenRowsUnderLimit() throws Exception {
        // Given: Dataset with less than 600k rows
        List<Map<String, Object>> testData = generateTestData(100000);

        // When: Writing results to CSV
        File result = invokeWriteResultsToCsv(testData);

        // Then: Should create single CSV file, not ZIP
        assertThat(result).isNotNull();
        assertThat(result.getName()).endsWith(".csv");
        assertThat(result.getName()).doesNotEndWith(".zip");

        // Verify CSV content
        List<String> lines = Files.readAllLines(result.toPath());
        // Header + 100k data rows
        assertThat(lines).hasSize(100001);
    }

    @Test
    void testMultipleCsvFilesWhenRowsOverLimit() throws Exception {
        // Given: Dataset with more than 600k rows (1.5 million)
        List<Map<String, Object>> testData = generateTestData(1500000);

        // When: Writing results to CSV
        File result = invokeWriteResultsToCsv(testData);

        // Then: Should create ZIP file
        assertThat(result).isNotNull();
        assertThat(result.getName()).endsWith(".zip");

        // Verify ZIP contains 3 CSV files (600k + 600k + 300k)
        try (ZipFile zipFile = new ZipFile(result)) {
            assertThat(Collections.list(zipFile.entries())).hasSize(3);

            zipFile.entries().asIterator().forEachRemaining(entry -> {
                assertThat(entry.getName()).endsWith(".csv");
                assertThat(entry.getName()).contains("part");
            });
        }
    }

    @Test
    void testExactlyMaxRowsPerCsvFile() throws Exception {
        // Given: Exactly 600k rows
        List<Map<String, Object>> testData = generateTestData(MAX_ROWS_PER_CSV);

        // When: Writing results to CSV
        File result = invokeWriteResultsToCsv(testData);

        // Then: Should create single CSV file
        assertThat(result).isNotNull();
        assertThat(result.getName()).endsWith(".csv");

        List<String> lines = Files.readAllLines(result.toPath());
        assertThat(lines).hasSize(MAX_ROWS_PER_CSV + 1); // +1 for header
    }

    @Test
    void testChunkingPreservesHeaders() throws Exception {
        // Given: Dataset that requires chunking
        List<Map<String, Object>> testData = generateTestData(1200000);

        // When: Writing results to CSV
        File result = invokeWriteResultsToCsv(testData);

        // Then: Each CSV in ZIP should have headers
        try (ZipFile zipFile = new ZipFile(result)) {
            zipFile.entries().asIterator().forEachRemaining(entry -> {
                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(zipFile.getInputStream(entry)))) {
                    String firstLine = reader.readLine();
                    // Verify header exists
                    assertThat(firstLine).contains("column1,column2,column3");
                } catch (IOException e) {
                    fail("Failed to read ZIP entry: " + e.getMessage());
                }
            });
        }
    }

    @Test
    void testEmptyResultsHandling() throws Exception {
        // Given: Empty dataset
        List<Map<String, Object>> testData = new ArrayList<>();

        // When: Writing results to CSV
        File result = invokeWriteResultsToCsv(testData);

        // Then: Should create empty CSV file
        assertThat(result).isNotNull();
        assertThat(result.getName()).endsWith(".csv");

        List<String> lines = Files.readAllLines(result.toPath());
        assertThat(lines).isEmpty();
    }

    @Test
    void testLargeDatasetChunking() throws Exception {
        // Given: 2 million rows (should create 4 files)
        List<Map<String, Object>> testData = generateTestData(2000000);

        // When: Writing results to CSV
        File result = invokeWriteResultsToCsv(testData);

        // Then: Should create ZIP with 4 CSV files
        assertThat(result.getName()).endsWith(".zip");

        try (ZipFile zipFile = new ZipFile(result)) {
            List<? extends ZipEntry> entries = Collections.list(zipFile.entries());
            assertThat(entries).hasSize(4);

            // Verify file names are sequential
            assertThat(entries.get(0).getName()).contains("part1");
            assertThat(entries.get(1).getName()).contains("part2");
            assertThat(entries.get(2).getName()).contains("part3");
            assertThat(entries.get(3).getName()).contains("part4");
        }
    }

    @Test
    void testCsvFieldFormatting() throws Exception {
        // Given: Data with special characters that need escaping
        List<Map<String, Object>> testData = new ArrayList<>();
        // Use LinkedHashMap to preserve field order
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("normal", "value");
        row.put("withQuote", "value\"with\"quotes");
        row.put("withComma", "value,with,commas");
        row.put("withNewline", "value\nwith\nnewlines");
        row.put("nullValue", null);
        testData.add(row);

        // When: Writing results to CSV
        File result = invokeWriteResultsToCsv(testData);

        // Then: Fields should be properly escaped
        String content = Files.readString(result.toPath());
        // Verify proper escaping
        assertThat(content).contains("\"value\"\"with\"\"quotes\"");
        assertThat(content).contains("\"value,with,commas\"");
        assertThat(content).contains("\"\""); // null value
    }

    @Test
    void testSqlResultsToSingleFile() throws Exception {
        // Given: SQL results under the limit
        List<String[]> sqlResults = generateSqlTestData(50000);

        // When: Writing SQL results to CSV
        File result = invokeWriteSingleSqlCsvFile(sqlResults, "test-", ".csv");

        // Then: Should create single CSV file
        assertThat(result).isNotNull();
        assertThat(result.getName()).startsWith("test-");
        assertThat(result.getName()).endsWith(".csv");

        List<String> lines = Files.readAllLines(result.toPath());
        assertThat(lines).hasSize(50000);
    }

    @Test
    void testZipFileContentsIntegrity() throws Exception {
        // Given: Large dataset
        List<Map<String, Object>> testData = generateTestData(1200000);

        // When: Creating ZIP file
        File result = invokeWriteResultsToCsv(testData);

        // Then: Verify total row count across all files in ZIP
        int totalRows = 0;
        try (ZipFile zipFile = new ZipFile(result)) {
            for (ZipEntry entry : Collections.list(zipFile.entries())) {
                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(zipFile.getInputStream(entry)))) {
                    // Count lines, excluding header
                    long lines = reader.lines().count() - 1;
                    totalRows += lines;
                }
            }
        }

        assertThat(totalRows).isEqualTo(1200000);
    }

    // Helper methods

    private List<Map<String, Object>> generateTestData(int rows) {
        List<Map<String, Object>> data = new ArrayList<>();
        for (int i = 0; i < rows; i++) {
            // Use LinkedHashMap to preserve order
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("column1", "value" + i);
            row.put("column2", i);
            row.put("column3", "test" + i);
            data.add(row);
        }
        return data;
    }

    private List<String[]> generateSqlTestData(int rows) {
        List<String[]> data = new ArrayList<>();
        // Add header
        data.add(new String[]{"column1", "column2", "column3"});
        // Add data rows
        for (int i = 0; i < rows - 1; i++) {
            data.add(new String[]{"value" + i, String.valueOf(i), "test" + i});
        }
        return data;
    }

    // Reflection-based invocation of private methods for testing
    private File invokeWriteResultsToCsv(List<Map<String, Object>> results) throws Exception {
        java.lang.reflect.Method method = DownloadService.class.getDeclaredMethod(
                "writeResultsToCsv", List.class, int.class);
        method.setAccessible(true);
        return (File) method.invoke(downloadService, results, 600000);
    }

    private File invokeWriteSingleSqlCsvFile(List<String[]> results, String prefix, String suffix) throws Exception {
        java.lang.reflect.Method method = DownloadService.class.getDeclaredMethod(
                "writeSingleSqlCsvFile", List.class, String.class, String.class);
        method.setAccessible(true);
        return (File) method.invoke(downloadService, results, prefix, suffix);
    }
}