package com.easyagents.skill.store.file;

import com.easyagents.skill.codec.ZipSkillPackageCodec;
import com.easyagents.skill.exception.SkillException;
import com.easyagents.skill.store.SkillContentStage;
import com.easyagents.skill.store.SkillContentStore;
import com.easyagents.skill.util.SkillHashes;
import org.junit.Assert;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * {@link TemporaryFileSkillContentStore} 流式存储与生命周期测试。
 */
public class TemporaryFileSkillContentStoreTest {

    /**
     * 暂存、提交、回滚、去重与引用计数均由磁盘文件驱动。
     *
     * @throws Exception 文件操作失败时抛出
     */
    @Test
    public void stageCommitRollbackAndReferenceCountingAreFileBacked() throws Exception {
        Path parent = Files.createTempDirectory("skill-content-store-test-");
        TemporaryFileSkillContentStore store = new TemporaryFileSkillContentStore(parent);
        Path storageDirectory = store.storageDirectory();
        byte[] content = "file-backed-content".getBytes(StandardCharsets.UTF_8);
        try {
            SkillContentStage rolledBack = store.stage(
                    new ByteArrayInputStream(content), content.length);
            Assert.assertFalse(store.exists(rolledBack.getContentRef()));
            Assert.assertEquals(1, countRegularFiles(storageDirectory.resolve("staged")));

            store.rollback(rolledBack);
            Assert.assertEquals(0, countRegularFiles(storageDirectory.resolve("staged")));

            SkillContentStage firstStage = store.stage(
                    new ByteArrayInputStream(content), content.length);
            SkillContentStage secondStage = store.stage(
                    new ByteArrayInputStream(content), content.length);
            String firstRef = store.commit(firstStage);
            String secondRef = store.commit(secondStage);

            Assert.assertEquals(firstRef, secondRef);
            Assert.assertTrue(store.exists(firstRef));
            Assert.assertEquals(0, countRegularFiles(storageDirectory.resolve("staged")));
            Assert.assertEquals(1, countRegularFiles(storageDirectory.resolve("content")));

            store.release(firstRef);
            Assert.assertTrue(store.exists(secondRef));
            store.retain(secondRef);
            store.release(secondRef);
            Assert.assertTrue(store.exists(secondRef));
            store.release(secondRef);

            Assert.assertFalse(store.exists(secondRef));
            Assert.assertEquals(0, countRegularFiles(storageDirectory.resolve("content")));
        } finally {
            store.close();
            Assert.assertFalse(Files.exists(storageDirectory));
            Assert.assertTrue(Files.exists(parent));
            Files.deleteIfExists(parent);
        }
    }

    /**
     * 已打开的流持有读取租约，最后一个正式引用释放后等待流关闭再删除文件。
     *
     * @throws Exception 文件读取失败时抛出
     */
    @Test
    public void openStreamDefersPhysicalDeletionUntilReaderCloses() throws Exception {
        Path parent = Files.createTempDirectory("skill-content-reader-test-");
        try (TemporaryFileSkillContentStore store = new TemporaryFileSkillContentStore(parent)) {
            Path contentDirectory = store.storageDirectory().resolve("content");
            byte[] content = "leased-content".getBytes(StandardCharsets.UTF_8);
            String contentRef = store.put(content);
            InputStream input = store.open(contentRef);

            store.release(contentRef);
            Assert.assertFalse(store.exists(contentRef));
            Assert.assertEquals(1, countRegularFiles(contentDirectory));
            Assert.assertArrayEquals(content, input.readAllBytes());

            input.close();
            Assert.assertEquals(0, countRegularFiles(contentDirectory));

            String pendingRef = store.put(content);
            InputStream pendingReader = store.open(pendingRef);
            store.release(pendingRef);
            String revivedRef = store.put(content);
            Assert.assertEquals(pendingRef, revivedRef);

            pendingReader.close();
            Assert.assertTrue(store.exists(revivedRef));
            Assert.assertEquals(1, countRegularFiles(contentDirectory));
            store.release(revivedRef);
            Assert.assertEquals(0, countRegularFiles(contentDirectory));
        } finally {
            Files.deleteIfExists(parent);
        }
    }

    /**
     * 大内容按流写入并受字节上限约束，存储实现及内部状态不持有 byte[] 字段。
     *
     * @throws Exception 文件操作失败时抛出
     */
    @Test
    public void largeContentIsStreamedWithoutByteArrayState() throws Exception {
        assertNoByteArrayFields(TemporaryFileSkillContentStore.class);
        long contentSize = 2L * 1024 * 1024 + 37;
        Path parent = Files.createTempDirectory("skill-content-stream-test-");
        try (TemporaryFileSkillContentStore store = new TemporaryFileSkillContentStore(parent)) {
            SkillContentStage stage = store.stage(new RepeatingInputStream(contentSize), contentSize);
            Assert.assertEquals(contentSize, stage.getSize());
            String contentRef = store.commit(stage);

            MessageDigest digest = SkillHashes.newSha256Digest();
            long copied = 0;
            try (InputStream input = store.open(contentRef)) {
                byte[] buffer = new byte[8192];
                int length;
                while ((length = input.read(buffer)) >= 0) {
                    if (length == 0) {
                        continue;
                    }
                    digest.update(buffer, 0, length);
                    copied += length;
                }
            }
            Assert.assertEquals(contentSize, copied);
            Assert.assertEquals(stage.getContentHash(), SkillHashes.toHex(digest.digest()));

            assertLimitExceeded(store, contentSize - 1);
            Assert.assertEquals(0,
                    countRegularFiles(store.storageDirectory().resolve("staged")));
            store.release(contentRef);
        } finally {
            Files.deleteIfExists(parent);
        }
    }

    /**
     * 无参 ZIP Codec 使用自有临时文件 Store，注入 Store 仍由调用方管理。
     *
     * @throws Exception 反射或文件操作失败时抛出
     */
    @Test
    public void defaultCodecOwnsTemporaryStoreWhileInjectedStoreRemainsExternal()
            throws Exception {
        Field contentStoreField = ZipSkillPackageCodec.class.getDeclaredField("contentStore");
        contentStoreField.setAccessible(true);

        ZipSkillPackageCodec defaultCodec = new ZipSkillPackageCodec();
        SkillContentStore defaultStore = (SkillContentStore) contentStoreField.get(defaultCodec);
        Assert.assertTrue(defaultStore instanceof TemporaryFileSkillContentStore);
        Path defaultDirectory = ((TemporaryFileSkillContentStore) defaultStore).storageDirectory();
        defaultCodec.close();
        Assert.assertFalse(Files.exists(defaultDirectory));

        Path parent = Files.createTempDirectory("skill-codec-external-store-test-");
        TemporaryFileSkillContentStore injectedStore = new TemporaryFileSkillContentStore(parent);
        try {
            ZipSkillPackageCodec injectedCodec = new ZipSkillPackageCodec(injectedStore);
            injectedCodec.close();
            String contentRef = injectedStore.put("still-open".getBytes(StandardCharsets.UTF_8));
            Assert.assertTrue(injectedStore.exists(contentRef));
            injectedStore.release(contentRef);
        } finally {
            injectedStore.close();
            Files.deleteIfExists(parent);
        }
    }

    /**
     * 显式关闭后清理暂存文件并拒绝新的写入。
     *
     * @throws Exception 文件操作失败时抛出
     */
    @Test
    public void closeCleansStagesAndRejectsFurtherOperations() throws Exception {
        Path parent = Files.createTempDirectory("skill-content-close-test-");
        TemporaryFileSkillContentStore store = new TemporaryFileSkillContentStore(parent);
        Path storageDirectory = store.storageDirectory();
        store.stage(new ByteArrayInputStream(new byte[]{1, 2, 3}), 3);

        store.close();
        store.close();

        Assert.assertFalse(Files.exists(storageDirectory));
        try {
            store.stage(new ByteArrayInputStream(new byte[0]), 0);
            Assert.fail("Closed store should reject new stages.");
        } catch (SkillException expected) {
            Assert.assertTrue(expected.getMessage().contains("closed"));
        } finally {
            Files.deleteIfExists(parent);
        }
    }

    private static void assertLimitExceeded(TemporaryFileSkillContentStore store, long maxBytes) {
        try {
            store.stage(new RepeatingInputStream(maxBytes + 1), maxBytes);
            Assert.fail("Content above the limit should be rejected.");
        } catch (SkillException expected) {
            Assert.assertTrue(expected.getMessage().contains("exceeds"));
        }
    }

    private static void assertNoByteArrayFields(Class<?> rootType) {
        List<Class<?>> types = new ArrayList<>();
        types.add(rootType);
        for (int index = 0; index < types.size(); index++) {
            Class<?> type = types.get(index);
            for (Field field : type.getDeclaredFields()) {
                Assert.assertNotEquals(type.getName() + " must not retain byte[] field "
                        + field.getName(), byte[].class, field.getType());
            }
            for (Class<?> nestedType : type.getDeclaredClasses()) {
                types.add(nestedType);
            }
        }
    }

    private static long countRegularFiles(Path directory) throws IOException {
        try (Stream<Path> paths = Files.list(directory)) {
            return paths.filter(Files::isRegularFile).count();
        }
    }

    private static final class RepeatingInputStream extends InputStream {

        private final long length;
        private long position;

        private RepeatingInputStream(long length) {
            this.length = length;
        }

        @Override
        public int read() {
            if (position >= length) {
                return -1;
            }
            return (int) (position++ & 0xFF);
        }

        @Override
        public int read(byte[] bytes, int offset, int requestedLength) {
            if (position >= length) {
                return -1;
            }
            int actualLength = (int) Math.min(requestedLength, length - position);
            for (int index = 0; index < actualLength; index++) {
                bytes[offset + index] = (byte) ((position + index) & 0xFF);
            }
            position += actualLength;
            return actualLength;
        }
    }
}
