package tech.easyflow.autoconfig.license;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * {@link EasyflowLicenseVerifier} 测试。
 */
public class EasyflowLicenseVerifierTest {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-10T12:00:00Z"), ZoneOffset.UTC);

    /**
     * 合法 license 应校验通过。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldVerifyValidLicense() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        EasyflowLicenseVerificationResult result = verifier.verify(files.licenseLocation);

        Assert.assertEquals("LIC-20260510-120000", result.licenseId());
        Assert.assertEquals("prod-2026-01", result.keyId());
        Assert.assertEquals("STANDARD", result.licenseType());
    }

    /**
     * 当配置目录时，应自动找到唯一的 .lic 文件。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldResolveSingleLicFileFromDirectory() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        EasyflowLicenseVerificationResult result = verifier.verify("file:" + Path.of(files.licensePath).getParent());

        Assert.assertEquals("LIC-20260510-120000", result.licenseId());
    }

    /**
     * 当配置的固定文件不存在时，应回退到同目录唯一的 .lic 文件。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFallbackToSingleLicFileInSameDirectory() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        EasyflowLicenseVerificationResult result = verifier.verify("file:" + Path.of(files.licensePath).getParent().resolve("easyflow.lic"));

        Assert.assertEquals("LIC-20260510-120000", result.licenseId());
    }

    /**
     * 缺失 license 文件时应抛出缺失异常。
     */
    @Test
    public void shouldFailWhenLicenseFileMissing() {
        EasyflowLicenseVerifier verifier = createVerifier("classpath:missing-public.pem",
                new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF"));

        try {
            verifier.verify("file:/path/not-exists.lic");
            Assert.fail("预期应抛出异常");
        } catch (EasyflowLicenseException ex) {
            Assert.assertTrue(ex.isMissing());
            Assert.assertTrue(ex.getMessage().contains("不存在"));
        }
    }

    /**
     * 同目录存在多个 .lic 文件时应报定位失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenMultipleLicFilesExist() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);
        Files.writeString(Path.of(files.licensePath).getParent().resolve("other.lic"), "dummy", StandardCharsets.UTF_8);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        try {
            verifier.verify("file:" + Path.of(files.licensePath).getParent());
            Assert.fail("预期应抛出异常");
        } catch (EasyflowLicenseException ex) {
            Assert.assertTrue(ex.isMissing());
            Assert.assertTrue(ex.getMessage().contains("多个 .lic"));
        }
    }

    /**
     * 非法前缀应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenPrefixInvalid() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, true);
        Files.writeString(Path.of(files.licensePath), "BAD." + Files.readString(Path.of(files.licensePath)), StandardCharsets.UTF_8);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        try {
            verifier.verify(files.licenseLocation);
            Assert.fail("预期应抛出异常");
        } catch (EasyflowLicenseException ex) {
            Assert.assertFalse(ex.isMissing());
            Assert.assertTrue(ex.getMessage().contains("前缀"));
        }
    }

    /**
     * 非法 product 应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenProductInvalid() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "OtherFlow"), identity, false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        try {
            verifier.verify(files.licenseLocation);
            Assert.fail("预期应抛出异常");
        } catch (EasyflowLicenseException ex) {
            Assert.assertTrue(ex.getMessage().contains("product"));
        }
    }

    /**
     * 已过期的 STANDARD license 应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenLicenseExpired() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-05-01T00:00:00Z", "EasyFlow"), identity, false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        try {
            verifier.verify(files.licenseLocation);
            Assert.fail("预期应抛出异常");
        } catch (EasyflowLicenseException ex) {
            Assert.assertTrue(ex.getMessage().contains("过期"));
        }
    }

    /**
     * DEV license 可不包含 expiresAt。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldAllowDevLicenseWithoutExpiresAt() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "DEV", null, "EasyFlow"), identity, false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        EasyflowLicenseVerificationResult result = verifier.verify(files.licenseLocation);
        Assert.assertNull(result.expiresAt());
    }

    /**
     * 机器指纹不匹配时应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenFingerprintMismatch() throws Exception {
        MachineIdentity licenseIdentity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        MachineIdentity runtimeIdentity = new MachineIdentity("machine-two", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(licenseIdentity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), licenseIdentity, false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, runtimeIdentity);
        try {
            verifier.verify(files.licenseLocation);
            Assert.fail("预期应抛出异常");
        } catch (EasyflowLicenseException ex) {
            Assert.assertTrue(ex.getMessage().contains("指纹不匹配"));
        }
    }

    /**
     * classpath 资源应可被正确读取并校验。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldVerifyClasspathLicense() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);
        writeClasspathTestResource("test-license/public.pem", Files.readString(Path.of(files.publicKeyPath), StandardCharsets.UTF_8));
        writeClasspathTestResource("test-license/license.lic", Files.readString(Path.of(files.licensePath), StandardCharsets.UTF_8));

        EasyflowLicenseVerifier verifier = createVerifier("classpath:test-license/public.pem", identity);
        EasyflowLicenseVerificationResult result = verifier.verify("classpath:test-license/license.lic");
        Assert.assertEquals("LIC-20260510-120000", result.licenseId());
    }

    /**
     * formatVersion 非 1 时应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenFormatVersionInvalid() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
        TestLicenseFiles files = createLicenseFiles(payload, identity, false, 2, "Ed25519", payload.get("fingerprintAlgorithm").toString(), false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        assertInvalid(verifier, files.licenseLocation, "formatVersion");
    }

    /**
     * alg 非 Ed25519 时应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenAlgorithmInvalid() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
        TestLicenseFiles files = createLicenseFiles(payload, identity, false, 1, "RSA", payload.get("fingerprintAlgorithm").toString(), false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        assertInvalid(verifier, files.licenseLocation, "签名算法");
    }

    /**
     * fingerprintAlgorithm 不匹配时应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenFingerprintAlgorithmInvalid() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
        TestLicenseFiles files = createLicenseFiles(payload, identity, false, 1, "Ed25519", "sha256(other)", false);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        assertInvalid(verifier, files.licenseLocation, "fingerprintAlgorithm");
    }

    /**
     * 篡改签名后应校验失败。
     *
     * @throws Exception 测试失败
     */
    @Test
    public void shouldFailWhenSignatureTampered() throws Exception {
        MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
        Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
        TestLicenseFiles files = createLicenseFiles(payload, identity, false, 1, "Ed25519", payload.get("fingerprintAlgorithm").toString(), true);

        EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
        assertInvalid(verifier, files.licenseLocation, "签名校验失败");
    }

    /**
     * MAC 地址应完成归一化、去重与排序。
     */
    @Test
    public void shouldNormalizeMacAddresses() {
        Assert.assertEquals("aa:bb:cc:dd:ee:ff",
                EasyflowLicenseVerifier.normalizeMacAddress("AA-BB-CC-DD-EE-FF"));
        Assert.assertEquals(
                java.util.List.of("11:22:33:44:55:66", "aa:bb:cc:dd:ee:ff"),
                EasyflowLicenseVerifier.normalizeMacAddresses("AA-BB-CC-DD-EE-FF\n11:22:33:44:55:66\naa:bb:cc:dd:ee:ff"));
    }

    /**
     * 启动前校验器应在缺失 license 时直接失败。
     */
    @Test
    public void shouldFailBootstrapValidatorWhenLicenseMissing() {
        EasyflowLicenseBootstrapValidator validator = new EasyflowLicenseBootstrapValidator(
                createVerifier("classpath:missing-public.pem",
                        new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF"))
        );
        StandardEnvironment environment = new StandardEnvironment();
        environment.getPropertySources().addFirst(
                new MapPropertySource("test", Map.of("easyflow.license.location", "file:/path/not-exists.lic"))
        );
        validator.setEnvironment(environment);

        try {
            validator.postProcessBeanFactory(null);
            Assert.fail("预期应抛出异常");
        } catch (IllegalStateException ex) {
            Assert.assertTrue(ex.getMessage().contains("不存在") || ex.getMessage().contains("未配置"));
        }
    }

    private EasyflowLicenseVerifier createVerifier(String publicKeyLocation, MachineIdentity identity) {
        return new EasyflowLicenseVerifier(
                new DefaultResourceLoader(),
                new StubMachineIdentityCollector(identity),
                FIXED_CLOCK,
                publicKeyLocation
        );
    }

    private TestLicenseFiles createLicenseFiles(Map<String, Object> payload,
                                                MachineIdentity identity,
                                                boolean keepOriginalPrefix) throws Exception {
        return createLicenseFiles(payload, identity, keepOriginalPrefix, 1, "Ed25519",
                payload.get("fingerprintAlgorithm").toString(), false);
    }

    private TestLicenseFiles createLicenseFiles(Map<String, Object> payload,
                                                MachineIdentity identity,
                                                boolean keepOriginalPrefix,
                                                int formatVersion,
                                                String algorithm,
                                                String fingerprintAlgorithm,
                                                boolean tamperSignature) throws Exception {
        KeyPair keyPair = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
        payload.put("fingerprintAlgorithm", fingerprintAlgorithm);
        String canonicalPayload = buildCanonicalPayloadJson(payload);
        String signature = sign(canonicalPayload, keyPair);
        if (tamperSignature) {
            signature = Base64.getEncoder().encodeToString("tampered-signature".getBytes(StandardCharsets.UTF_8));
        }

        Map<String, Object> envelope = new LinkedHashMap<>();
        envelope.put("formatVersion", formatVersion);
        envelope.put("alg", algorithm);
        envelope.put("keyId", "prod-2026-01");
        envelope.put("payload", payload);
        envelope.put("signature", signature);

        String licenseContent = EasyflowLicenseVerifier.LICENSE_FILE_PREFIX
                + Base64.getUrlEncoder().withoutPadding()
                .encodeToString(OBJECT_MAPPER.writeValueAsBytes(envelope));
        if (!keepOriginalPrefix) {
            licenseContent = licenseContent.trim();
        }

        Path tempDir = Files.createTempDirectory("easyflow-license-test");
        Path publicKeyPath = tempDir.resolve("public.pem");
        Path licensePath = tempDir.resolve("license.lic");
        Files.writeString(publicKeyPath, toPublicPem(keyPair), StandardCharsets.UTF_8);
        Files.writeString(licensePath, licenseContent, StandardCharsets.UTF_8);
        return new TestLicenseFiles(publicKeyPath.toUri().toString(),
                licensePath.toUri().toString(),
                publicKeyPath.toString(),
                licensePath.toString());
    }

    private Map<String, Object> createPayload(MachineIdentity identity,
                                              String licenseType,
                                              String expiresAt,
                                              String product) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("licenseId", "LIC-20260510-120000");
        payload.put("product", product);
        payload.put("customerName", "Test Customer");
        payload.put("environment", "PROD");
        payload.put("licenseType", licenseType);
        payload.put("issuedAt", "2026-05-10T12:00:00Z");
        payload.put("expiresAt", expiresAt);
        payload.put("fingerprintAlgorithm", EasyflowLicenseVerifier.FINGERPRINT_ALGORITHM);
        payload.put("machineFingerprint", EasyflowLicenseVerifier.computeMachineFingerprint(identity));
        return payload;
    }

    private String buildCanonicalPayloadJson(Map<String, Object> payload) {
        return "{"
                + "\"customerName\":\"" + payload.get("customerName") + "\","
                + "\"environment\":\"" + payload.get("environment") + "\","
                + "\"expiresAt\":" + quoteOrNull(payload.get("expiresAt")) + ","
                + "\"fingerprintAlgorithm\":\"" + payload.get("fingerprintAlgorithm") + "\","
                + "\"issuedAt\":\"" + payload.get("issuedAt") + "\","
                + "\"licenseId\":\"" + payload.get("licenseId") + "\","
                + "\"licenseType\":\"" + payload.get("licenseType") + "\","
                + "\"machineFingerprint\":\"" + payload.get("machineFingerprint") + "\","
                + "\"product\":\"" + payload.get("product") + "\""
                + "}";
    }

    private String quoteOrNull(Object value) {
        return value == null ? "null" : "\"" + value + "\"";
    }

    private String sign(String canonicalPayload, KeyPair keyPair) throws Exception {
        Signature signer = Signature.getInstance("Ed25519");
        signer.initSign(keyPair.getPrivate());
        signer.update(canonicalPayload.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signer.sign());
    }

    private String toPublicPem(KeyPair keyPair) {
        String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
                .encodeToString(keyPair.getPublic().getEncoded());
        return "-----BEGIN PUBLIC KEY-----\n" + base64 + "\n-----END PUBLIC KEY-----\n";
    }

    private void assertInvalid(EasyflowLicenseVerifier verifier, String location, String expectedMessagePart) {
        try {
            verifier.verify(location);
            Assert.fail("预期应抛出异常");
        } catch (EasyflowLicenseException ex) {
            Assert.assertTrue(ex.getMessage().contains(expectedMessagePart));
        }
    }

    private void writeClasspathTestResource(String relativePath, String content) throws Exception {
        Path path = Path.of("target/test-classes").resolve(relativePath);
        Files.createDirectories(path.getParent());
        Files.writeString(path, content, StandardCharsets.UTF_8);
    }

    /**
     * 测试用固定机器信息采集器。
     */
    private static class StubMachineIdentityCollector extends MachineIdentityCollector {

        private final MachineIdentity identity;

        private StubMachineIdentityCollector(MachineIdentity identity) {
            this.identity = identity;
        }

        /**
         * 直接返回固定机器信息。
         *
         * @return 固定机器信息
         */
        @Override
        public MachineIdentity collect() {
            return identity;
        }
    }

    /**
     * 测试临时文件信息。
     *
     * @param publicKeyLocation 公钥位置
     * @param licenseLocation license 位置
     * @param licensePath license 文件路径
     */
    private record TestLicenseFiles(String publicKeyLocation,
                                    String licenseLocation,
                                    String publicKeyPath,
                                    String licensePath) {
    }
}
