package org.example;

import jakarta.servlet.ServletContext;
import org.example.Api.PublicApiRegistry;
import org.example.Auth.controller.AuthController;
import org.example.Entity.System.LoginUserInfo;
import org.example.Config.CorsConfig;
import org.example.Impl.BaseImpl;
import org.example.ModuleApi.ModuleAjaxApi.controller.ModuleAjaxController;
import org.example.SystemApi.controller.SystemAjaxApi;
import org.example.Utils.FileUtil;
import org.example.Utils.JwtHelp;
import org.example.Utils.RemoteDownloadGuard;
import org.example.Utils.SqlSafetyGuard;
import org.example.Utils.ZipUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Field;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class SecurityRegressionTest {

    @TempDir
    Path tempDir;

    @AfterEach
    void clearRequestContext() {
        RequestContextHolder.resetRequestAttributes();
    }

    @Test
    void fileAuthorityRejectsPathThatOnlyContainsAllowedRootName() throws IOException {
        Path allowedRootNameInsideAnotherDirectory = tempDir.resolve("home").resolve("Lserp").resolve("fileRoot_evil");
        Path file = allowedRootNameInsideAnotherDirectory.resolve("payload.txt");
        Files.createDirectories(file.getParent());
        Files.writeString(file, "payload");

        assertFalse(FileUtil.checkFileAuthory(file.toString(), ""));
    }

    @Test
    void unzipRejectsEntriesOutsideTargetDirectory() throws IOException {
        Path zip = tempDir.resolve("archive.zip");
        Path target = tempDir.resolve("extract");
        Path escaped = tempDir.resolve("escaped.txt");

        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zip))) {
            zos.putNextEntry(new ZipEntry("../escaped.txt"));
            zos.write("escaped".getBytes());
            zos.closeEntry();
        }

        assertFalse(ZipUtil.UnZip(zip.toString(), target.toString(), ""));
        assertFalse(Files.exists(escaped));
    }

    @Test
    void iosAndAndroidDriversAreRecognizedAsPhoneClients() {
        bindRequestWithDriver("ios");
        assertTrue(new BaseImpl().isPhone());

        bindRequestWithDriver("android");
        assertTrue(new BaseImpl().isPhone());
    }

    @Test
    void userConnectionStringIsUsedWhenAccountHasOne() {
        LoginUserInfo user = new LoginUserInfo();
        user.UserId = "1001";
        user.ConnectionString = "jdbc:sqlserver://db;DatabaseName=tenant";

        TestableBaseImpl base = new TestableBaseImpl();
        base.setUser(user);

        assertEquals(user.ConnectionString, base.connectionString());
    }

    @Test
    void jwtHelpStaticCreationInitializesSigningKeyWhenSpringConstructorHasNotRun() throws Exception {
        resetJwtHelpKeyPair();
        LoginUserInfo user = new LoginUserInfo();
        user.UserId = "1001";
        user.UserCode = "zhangsan";
        user.UserName = "test-user";
        user.LoginOs = "web";
        user.ServerId = 0;

        String token = JwtHelp.createToken(user, 60);
        LoginUserInfo parsed = JwtHelp.validToken(token, LoginUserInfo.class);

        assertFalse(token == null || token.isBlank());
        assertNotNull(parsed);
        assertEquals(user.UserId, parsed.UserId);
    }

    @Test
    void corsWithCredentialsDoesNotExposeEveryResponseHeader() throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest("GET", "/Api/SystemAjaxApi");
        request.addHeader(HttpHeaders.ORIGIN, "http://localhost:8081");
        MockHttpServletResponse response = new MockHttpServletResponse();

        new CorsConfig().corsFilter().doFilter(request, response, new MockFilterChain());

        assertEquals("http://localhost:8081", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
        assertNotEquals("*", response.getHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS));
        assertTrue(response.getHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS).contains("Authorization"));
    }

    @Test
    void corsRejectsUnexpectedHttpMethods() throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/Api/SystemAjaxApi");
        request.addHeader(HttpHeaders.ORIGIN, "http://localhost:8081");
        request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "TRACE");
        MockHttpServletResponse response = new MockHttpServletResponse();

        new CorsConfig().corsFilter().doFilter(request, response, new MockFilterChain());

        assertEquals(403, response.getStatus());
        assertFalse(response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS));
    }

    @Test
    void remoteDownloadGuardRejectsPrivateAndMetadataAddresses() {
        assertFalse(RemoteDownloadGuard.isAllowedHttpUrl("http://127.0.0.1/file.txt"));
        assertFalse(RemoteDownloadGuard.isAllowedHttpUrl("http://localhost/file.txt"));
        assertFalse(RemoteDownloadGuard.isAllowedHttpUrl("http://192.168.1.20/file.txt"));
        assertFalse(RemoteDownloadGuard.isAllowedHttpUrl("http://10.0.0.5/file.txt"));
        assertFalse(RemoteDownloadGuard.isAllowedHttpUrl("http://172.16.0.10/file.txt"));
        assertFalse(RemoteDownloadGuard.isAllowedHttpUrl("http://169.254.169.254/latest/meta-data/"));
        assertFalse(RemoteDownloadGuard.isAllowedHttpUrl("file:///etc/passwd"));
    }

    @Test
    void remoteDownloadGuardAllowsPublicHttpAndHttpsUrls() {
        assertTrue(RemoteDownloadGuard.isAllowedHttpUrl("http://8.8.8.8/file.txt"));
        assertTrue(RemoteDownloadGuard.isAllowedHttpUrl("https://8.8.4.4/file.txt"));
    }

    @Test
    void sqlSafetyGuardAcceptsSafeIdentifiersAndReadOnlySelects() {
        assertTrue(SqlSafetyGuard.isSafeIdentifier("Field_Name1"));
        assertTrue(SqlSafetyGuard.isSafeIdentifier("dbo.Field_Name1"));
        assertTrue(SqlSafetyGuard.isSafeReadOnlySelect("select id, name from users where id = #{id}"));
        assertTrue(SqlSafetyGuard.isSafeReadOnlySelect("with active_users as (select id from users) select id from active_users"));
        assertTrue(SqlSafetyGuard.isSafeReadOnlySelect("select id from users union all select id from archived_users"));
        assertTrue(SqlSafetyGuard.isSafeReadOnlySelect("""
                select
                    id,
                    name
                from users
                """));
        assertTrue(SqlSafetyGuard.isSafeReadOnlySelect("""
                select id, name -- optional display columns
                from users
                where note = '-- this is data, not a comment'
                """));

        String sanitized = SqlSafetyGuard.requireSafeReadOnlySelect("""
                select id /* optional id column */, name
                from users -- optional source note
                where note = '/* this is data, not a comment */'
                """);
        assertFalse(sanitized.contains("optional id column"));
        assertFalse(sanitized.contains("optional source note"));
        assertTrue(sanitized.contains("'/* this is data, not a comment */'"));
    }

    @Test
    void sqlSafetyGuardKeepsMainQueryAfterLegacyInlineDisabledColumns() {
        String crmSql = "select case when ISNULL(a.crm_mpo_Affirmer,0) = 0 then '未提交' else '需：' + flow.stepName end as ztName, "
                + "a.crm_mpo_billdocument_id as Repeat_crm_mpo_billdocument_id, "
                + "a.crm_mpo_wasteoper, --mx.mxs, a.crm_mpo_OldSystemBill, crm_mpo_htname, crm_mpo_HtMoney, "
                + "--crm_lpo_summoney, --crm_lpo_nosummoney, crm_mpo_lyzlbzjpaytype, crm_mpo_ltono, "
                + "mx.*,(SELECT LISTAGG(p.spec,',') WITHIN GROUP (order by p.spec) "
                + "FROM Crm_BillPolisttab bp LEFT JOIN p_ProductTab p ON p.productid = bp.crm_lpo_productid "
                + "WHERE bp.crm_lpo_billdocument_id = a.crm_mpo_billdocument_id) AS cpxh "
                + "FROM Crm_BillPoMaintab a left join p_systembillflow flow with(nolock) "
                + "on a.crm_mpo_stepcode = flow.stepCode left join (select count(1) as mxs, crm_mpo_billdocument_id "
                + "from view_CRM_billpolisttab group by crm_mpo_billdocument_id) mx "
                + "on a.crm_mpo_billdocument_id = mx.crm_mpo_billdocument_id "
                + "where 1 = 1 and crm_mpo_ltono='SJ2026060006'";

        String sanitized = SqlSafetyGuard.requireSafeReadOnlySelect(crmSql);

        assertTrue(SqlSafetyGuard.isSafeReadOnlySelect(crmSql));
        assertTrue(sanitized.contains("FROM Crm_BillPoMaintab"));
        assertTrue(sanitized.contains("left join p_systembillflow"));
        assertTrue(sanitized.contains("where 1 = 1 and crm_mpo_ltono='SJ2026060006'"));
        assertTrue(sanitized.contains("a.crm_mpo_OldSystemBill"));
        assertTrue(sanitized.contains("crm_mpo_lyzlbzjpaytype"));
        assertFalse(sanitized.contains("--mx.mxs"));
        assertFalse(sanitized.contains("--crm_lpo_summoney"));
        assertFalse(sanitized.contains("--crm_lpo_nosummoney"));
    }

    @Test
    void sqlSafetyGuardKeepsCrmQueryBodyAfterMultilineDisabledColumns() {
        String crmSql = """
                select
                 case when ISNULL(a.crm_mpo_Affirmer,0) = 0 then 'draft' else flow.stepName end as ztName,
                 a.crm_mpo_billdocument_id as Repeat_crm_mpo_billdocument_id,
                 a.crm_mpo_wasteoper,
                 --mx.mxs,
                 a.crm_mpo_OldSystemBill,
                 crm_mpo_htname,
                 crm_mpo_HtMoney,
                 --crm_lpo_summoney,
                 --crm_lpo_nosummoney,
                 crm_mpo_lyzlbzjpaytype,
                 crm_mpo_ltono,
                 mx.*,
                 (SELECT LISTAGG(p.spec,',') WITHIN GROUP (order by p.spec)
                  FROM Crm_BillPolisttab bp
                  LEFT JOIN p_ProductTab p ON p.productid = bp.crm_lpo_productid
                  WHERE bp.crm_lpo_billdocument_id = a.crm_mpo_billdocument_id) AS cpxh
                FROM Crm_BillPoMaintab a
                left join p_systembillflow flow with(nolock)
                  on a.crm_mpo_stepcode = flow.stepCode and flow.typeCode = '121002'
                left join (
                  select count(1) as mxs, crm_mpo_billdocument_id
                  from view_CRM_billpolisttab
                  group by crm_mpo_billdocument_id
                ) mx on a.crm_mpo_billdocument_id = mx.crm_mpo_billdocument_id
                where 1 = 1 and crm_mpo_ltono='SJ2026060006'
                """;

        String sanitized = SqlSafetyGuard.requireSafeReadOnlySelect(crmSql);

        assertTrue(sanitized.contains("FROM Crm_BillPoMaintab"));
        assertTrue(sanitized.contains("left join p_systembillflow"));
        assertTrue(sanitized.contains("where 1 = 1 and crm_mpo_ltono='SJ2026060006'"));
        assertFalse(sanitized.contains("--mx.mxs"));
        assertFalse(sanitized.contains("--crm_lpo_summoney"));
        assertFalse(sanitized.contains("--crm_lpo_nosummoney"));
    }

    @Test
    void sqlSafetyGuardRejectsDangerousIdentifiersAndFragments() {
        assertFalse(SqlSafetyGuard.isSafeIdentifier("name;drop"));
        assertFalse(SqlSafetyGuard.isSafeIdentifier("1name"));
        assertFalse(SqlSafetyGuard.isSafeReadOnlySelect("select * from users; drop table users"));
        assertFalse(SqlSafetyGuard.isSafeReadOnlySelect("delete from users"));
        assertFalse(SqlSafetyGuard.isSafeReadOnlySelect("select * into backup_users from users"));
        assertFalse(SqlSafetyGuard.isSafeReadOnlySelect("select * from users where name = 'x' exec xp_cmdshell 'whoami'"));
        assertFalse(SqlSafetyGuard.isSafeConditionFragment("and status = 1 -- bypass"));
        assertFalse(SqlSafetyGuard.isSafeConditionFragment("union select password from users"));
    }

    @Test
    void publicApiRegistryKeepsOnlyBootstrapEndpointsPublic() {
        assertTrue(PublicApiRegistry.isPublicEndpoint(AuthController.class, "Login"));
        assertTrue(PublicApiRegistry.isPublicEndpoint(AuthController.class, "GenerateCaptcha"));
        assertTrue(PublicApiRegistry.isPublicEndpoint(SystemAjaxApi.class, "GetSystemLoginInfo"));
        assertFalse(PublicApiRegistry.isPublicEndpoint(ModuleAjaxController.class, "GetModuleData"));
        assertFalse(PublicApiRegistry.isPublicEndpoint(ModuleAjaxController.class, "GetModuleRightMenu"));
    }

    private static void bindRequestWithDriver(String driver) {
        ServletContext servletContext = new MockServletContext();
        MockHttpServletRequest request = new MockHttpServletRequest(servletContext);
        request.addParameter("driver", driver);
        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
    }

    private static void resetJwtHelpKeyPair() throws Exception {
        Field field = JwtHelp.class.getDeclaredField("rsaKeyPair");
        field.setAccessible(true);
        field.set(null, null);
    }

    private static class TestableBaseImpl extends BaseImpl {
        String connectionString() {
            return getConnectionString();
        }
    }
}
