package de.avatic.lcc.e2e.tests;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import de.avatic.lcc.LccApplication;
import de.avatic.lcc.config.DatabaseTestConfiguration;
import de.avatic.lcc.e2e.config.TestFrontendConfig;
import de.avatic.lcc.e2e.pages.DevLoginPage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
 * Abstract base class for E2E tests.
 * Starts Spring Boot backend with integrated frontend and provides Playwright setup.
 *
 * <p>Prerequisites:
 * <ul>
 *   <li>Frontend must be built to src/main/resources/static before running tests</li>
 *   <li>Run: {@code cd src/frontend && BUILD_FOR_SPRING=true npm run build}</li>
 * </ul>
 *
 * <p>Or use Maven profile (if configured):
 * {@code mvn test -Dtest="*E2ETest" -Pe2e}
 */
@SpringBootTest(
    classes = LccApplication.class,
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@Import({DatabaseTestConfiguration.class, TestFrontendConfig.class})
@Testcontainers
@ActiveProfiles({"test", "dev", "mysql", "e2e"})
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Tag("e2e")
public abstract class AbstractE2ETest {

    @Autowired
    protected JdbcTemplate jdbcTemplate;

    private static final Logger logger = Logger.getLogger(AbstractE2ETest.class.getName());

    protected static final boolean HEADLESS = Boolean.parseBoolean(
        System.getProperty("playwright.headless", "true")
    );
    protected static final double TOLERANCE = 0.03; // 3% tolerance for numeric comparisons

    @LocalServerPort
    protected int port;

    protected Playwright playwright;
    protected Browser browser;
    protected BrowserContext context;
    protected Page page;

    protected String getBaseUrl() {
        return "http://localhost:" + port;
    }

    @BeforeAll
    void setupPlaywright() {
        // Load E2E test data
        loadTestData();

        checkFrontendBuilt();

        logger.info("Setting up Playwright");
        playwright = Playwright.create();
        browser = playwright.chromium().launch(
            new BrowserType.LaunchOptions()
                .setHeadless(HEADLESS)
                .setSlowMo(HEADLESS ? 0 : 100)
        );

        // Ensure screenshot directory exists
        try {
            Files.createDirectories(Paths.get("target/screenshots"));
        } catch (Exception e) {
            logger.warning("Could not create screenshots directory");
        }

        logger.info(() -> String.format(
            "Playwright setup complete. Headless: %s, Base URL: %s",
            HEADLESS, getBaseUrl()
        ));
    }

    @BeforeEach
    void setupPage() {
        context = browser.newContext(new Browser.NewContextOptions()
            .setViewportSize(1920, 1080)
        );
        page = context.newPage();

        // Login via DevLoginPage
        DevLoginPage loginPage = new DevLoginPage(page);
        loginPage.login(getBaseUrl(), "John");

        // Navigate to home page after login
        page.navigate(getBaseUrl());
        page.waitForLoadState();

        // Take screenshot after login
        takeScreenshot("after_login");

        logger.info(() -> "Page setup complete, logged in as John. Current URL: " + page.url());
    }

    @AfterEach
    void teardownPage() {
        if (context != null) {
            context.close();
        }
    }

    @AfterAll
    void teardownPlaywright() {
        if (browser != null) {
            browser.close();
        }
        if (playwright != null) {
            playwright.close();
        }
        logger.info("Playwright teardown complete");
    }

    /**
     * Takes a screenshot for debugging purposes.
     */
    protected void takeScreenshot(String name) {
        Path screenshotPath = Paths.get("target/screenshots/" + name + ".png");
        page.screenshot(new Page.ScreenshotOptions().setPath(screenshotPath));
        logger.info(() -> "Screenshot saved to: " + screenshotPath);
    }

    /**
     * Checks if the frontend has been built to static resources.
     * Throws an exception with instructions if not.
     */
    private void checkFrontendBuilt() {
        Path staticIndex = Paths.get("src/main/resources/static/index.html");
        if (!Files.exists(staticIndex)) {
            // Try to build frontend automatically
            if (tryBuildFrontend()) {
                logger.info("Frontend built successfully");
            } else {
                throw new IllegalStateException(
                    "Frontend not built. Please run:\n" +
                    "  cd src/frontend && BUILD_FOR_SPRING=true npm run build\n" +
                    "Or set -Dskip.frontend.check=true to skip this check."
                );
            }
        } else {
            logger.info("Frontend already built at: " + staticIndex);
        }
    }

    /**
     * Attempts to build the frontend automatically.
     * Returns true if successful, false otherwise.
     */
    private boolean tryBuildFrontend() {
        if (Boolean.getBoolean("skip.frontend.build")) {
            return false;
        }

        logger.info("Attempting to build frontend...");

        try {
            File frontendDir = new File("src/frontend");
            if (!frontendDir.exists()) {
                logger.warning("Frontend directory not found");
                return false;
            }

            // Check if node_modules exists
            File nodeModules = new File(frontendDir, "node_modules");
            if (!nodeModules.exists()) {
                logger.info("Installing npm dependencies...");
                ProcessBuilder npmInstall = new ProcessBuilder("npm", "install")
                    .directory(frontendDir)
                    .inheritIO();
                Process installProcess = npmInstall.start();
                if (!installProcess.waitFor(5, TimeUnit.MINUTES)) {
                    installProcess.destroyForcibly();
                    return false;
                }
            }

            // Build frontend (to dist/)
            ProcessBuilder npmBuild = new ProcessBuilder("npm", "run", "build")
                .directory(frontendDir)
                .inheritIO();

            Process buildProcess = npmBuild.start();
            boolean completed = buildProcess.waitFor(3, TimeUnit.MINUTES);

            if (!completed) {
                buildProcess.destroyForcibly();
                return false;
            }

            if (buildProcess.exitValue() != 0) {
                return false;
            }

            // Copy dist/ to src/main/resources/static/
            return copyFrontendToStatic(frontendDir);

        } catch (IOException | InterruptedException e) {
            logger.warning("Failed to build frontend: " + e.getMessage());
            return false;
        }
    }

    /**
     * Loads E2E test data into the database.
     * This is called once before all tests run.
     */
    private void loadTestData() {
        logger.info("Loading E2E test data...");

        // Check if test users already exist
        Integer existingUsers = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_user WHERE email = 'john.doe@test.com'",
            Integer.class
        );

        if (existingUsers != null && existingUsers > 0) {
            logger.info("Test users already exist, checking nodes...");
            addMissingNodes();
            return;
        }

        // Create test users
        jdbcTemplate.update(
            "INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, ?)",
            "WD001TEST", "john.doe@test.com", "John", "Doe", true
        );
        jdbcTemplate.update(
            "INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, ?)",
            "WD002TEST", "jane.smith@test.com", "Jane", "Smith", true
        );
        jdbcTemplate.update(
            "INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, ?)",
            "WD003TEST", "admin.test@test.com", "Admin", "User", true
        );

        // Assign groups to users
        // John gets 'super' role for full E2E testing capabilities
        jdbcTemplate.update(
            "INSERT INTO sys_user_group_mapping (user_id, group_id) " +
            "SELECT u.id, g.id FROM sys_user u, sys_group g " +
            "WHERE u.email = 'john.doe@test.com' AND g.group_name = 'super'"
        );
        jdbcTemplate.update(
            "INSERT INTO sys_user_group_mapping (user_id, group_id) " +
            "SELECT u.id, g.id FROM sys_user u, sys_group g " +
            "WHERE u.email = 'jane.smith@test.com' AND g.group_name = 'super'"
        );
        jdbcTemplate.update(
            "INSERT INTO sys_user_group_mapping (user_id, group_id) " +
            "SELECT u.id, g.id FROM sys_user u, sys_group g " +
            "WHERE u.email = 'admin.test@test.com' AND g.group_name = 'super'"
        );

        // Add missing nodes for E2E tests
        addMissingNodes();

        logger.info("E2E test data loaded successfully");
    }

    /**
     * Adds missing nodes needed for E2E tests.
     */
    private void addMissingNodes() {
        logger.info("Adding missing nodes for E2E tests...");

        // Add Ireland supplier to node table (if not exists)
        Integer irelandCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM node WHERE name = 'Ireland supplier'", Integer.class);
        if (irelandCount == null || irelandCount == 0) {
            Integer ieCountryId = jdbcTemplate.queryForObject(
                "SELECT id FROM country WHERE iso_code = 'IE'", Integer.class);
            jdbcTemplate.update(
                "INSERT INTO node (country_id, name, address, external_mapping_id, predecessor_required, " +
                "is_destination, is_source, is_intermediate, geo_lat, geo_lng, is_deprecated) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                ieCountryId, "Ireland supplier", "Dublin Ireland", "IE_SUP", false,
                false, true, false, 53.3494, -6.2606, false
            );
            logger.info("Added Ireland supplier to node table");
        }

        // Get test user ID for sys_user_node entries
        Integer testUserId = jdbcTemplate.queryForObject(
            "SELECT id FROM sys_user WHERE email = 'john.doe@test.com'", Integer.class);

        // Add Turkey supplier to sys_user_node (if not exists)
        Integer turkeyCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_user_node WHERE name = 'Turkey supplier'", Integer.class);
        if (turkeyCount == null || turkeyCount == 0) {
            Integer trCountryId = jdbcTemplate.queryForObject(
                "SELECT id FROM country WHERE iso_code = 'TR'", Integer.class);
            jdbcTemplate.update(
                "INSERT INTO sys_user_node (user_id, country_id, name, address, geo_lat, geo_lng, is_deprecated) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?)",
                testUserId, trCountryId, "Turkey supplier", "Antalya Türkiye",
                36.8864, 30.7105, false
            );
            logger.info("Added Turkey supplier to sys_user_node table");
        }

        // Add Yantian supplier to sys_user_node (if not exists)
        Integer yantianCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM sys_user_node WHERE name = 'Yantian supplier'", Integer.class);
        if (yantianCount == null || yantianCount == 0) {
            Integer cnCountryId = jdbcTemplate.queryForObject(
                "SELECT id FROM country WHERE iso_code = 'CN'", Integer.class);
            jdbcTemplate.update(
                "INSERT INTO sys_user_node (user_id, country_id, name, address, geo_lat, geo_lng, is_deprecated) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?)",
                testUserId, cnCountryId, "Yantian supplier", "Yantian, China",
                22.5925, 114.2460, false
            );
            logger.info("Added Yantian supplier to sys_user_node table");
        }

        logger.info("Missing nodes added");

        // Add test materials
        addTestMaterials();
    }

    /**
     * Adds test materials needed for E2E tests.
     */
    private void addTestMaterials() {
        logger.info("Adding test materials...");

        String[] materials = {
            "3064540201", "003064540201", "84312000", "wheel hub",
            "4222640104", "004222640104", "84139100", "gearbox housing blank",
            "4222640803", "004222640803", "84139100", "planet gear carrier blank stage 1",
            "4222640805", "004222640805", "84139100", "planet gear carrier blank stage 2",
            "5512640106", "005512640106", "84312000", "transmission housing blank",
            "8212640113", "008212640113", "84312000", "transmission housing blank GR2E-04",
            "8212640827", "008212640827", "84312000", "planet gear carrier blank Stufe 1",
            "8222640822", "008222640822", "84839089", "planet gear carrier blank stage 1",
            "8263500575", "008263500575", "85015220", "traction motor assy"
        };

        for (int i = 0; i < materials.length; i += 4) {
            String partNumber = materials[i];
            String normalizedPartNumber = materials[i + 1];
            String hsCode = materials[i + 2];
            String name = materials[i + 3];

            // Check by normalized_part_number since that has the UNIQUE constraint
            Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM material WHERE normalized_part_number = ?",
                Integer.class, normalizedPartNumber);
            if (count == null || count == 0) {
                try {
                    jdbcTemplate.update(
                        "INSERT INTO material (part_number, normalized_part_number, hs_code, name, is_deprecated) " +
                        "VALUES (?, ?, ?, ?, ?)",
                        partNumber, normalizedPartNumber, hsCode, name, false
                    );
                    logger.info(() -> "Added material: " + partNumber + " (normalized: " + normalizedPartNumber + ")");
                } catch (Exception e) {
                    logger.warning(() -> "Failed to insert material " + partNumber + ": " + e.getMessage());
                }
            } else {
                logger.info(() -> "Material already exists: " + normalizedPartNumber);
            }
        }

        logger.info("Test materials added");
    }

    /**
     * Copies the built frontend from dist/ to src/main/resources/static/.
     */
    private boolean copyFrontendToStatic(File frontendDir) {
        Path source = frontendDir.toPath().resolve("dist");
        Path target = Paths.get("src/main/resources/static");

        if (!Files.exists(source)) {
            logger.warning("Frontend dist directory not found: " + source);
            return false;
        }

        try {
            // Create target directory if needed
            Files.createDirectories(target);

            // Copy all files recursively
            try (var walk = Files.walk(source)) {
                walk.forEach(sourcePath -> {
                    try {
                        Path targetPath = target.resolve(source.relativize(sourcePath));
                        if (Files.isDirectory(sourcePath)) {
                            Files.createDirectories(targetPath);
                        } else {
                            Files.copy(sourcePath, targetPath,
                                java.nio.file.StandardCopyOption.REPLACE_EXISTING);
                        }
                    } catch (IOException e) {
                        throw new RuntimeException("Failed to copy: " + sourcePath, e);
                    }
                });
            }

            logger.info("Frontend copied to: " + target);
            return true;

        } catch (IOException | RuntimeException e) {
            logger.warning("Failed to copy frontend: " + e.getMessage());
            return false;
        }
    }
}
