package tech.easyflow.common.filestorage.impl;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
import tech.easyflow.common.filestorage.FileStorageWriteResult;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

/**
 * {@link LocalFileStorageServiceImpl} 可恢复精确写删测试。
 */
public class LocalFileStorageServiceImplTest {

    /** 每个测试使用的隔离临时目录。 */
    @Rule
    public final TemporaryFolder temporaryFolder = new TemporaryFolder();

    /**
     * 验证固定位置原子写入、配置切换后仍按句柄根目录定位及幂等删除。
     *
     * @throws Exception 测试目录或反射配置失败
     */
    @Test
    public void recoverableWriteUsesPersistentRootAndDeletesIdempotently() throws Exception {
        File originalRoot = temporaryFolder.newFolder("original-root");
        File changedRoot = temporaryFolder.newFolder("changed-root");
        LocalFileStorageServiceImpl service = createService(originalRoot, "/files");
        FileStorageWriteHandle handle = service.prepareRecoverableWrite("skill-content/ab", "content.bin");
        setField(service, "root", changedRoot.getAbsolutePath());

        byte[] bytes = "recoverable-content".getBytes(java.nio.charset.StandardCharsets.UTF_8);
        FileStorageWriteResult result = service.saveRecoverable(new BytesMultipartFile(bytes), handle);
        Path target = Path.of(handle.getBasePath()).resolve(handle.getPath()).resolve(handle.getFilename());

        assertEquals("/files/skill-content/ab/content.bin", result.getUrl());
        assertEquals(handle, FileStorageWriteHandle.decodeLocator(result.getLocator()));
        assertTrue(service.existsRecoverable(handle));
        assertArrayEquals(bytes, Files.readAllBytes(target));
        try (InputStream inputStream = service.readRecoverable(handle)) {
            assertArrayEquals(bytes, inputStream.readAllBytes());
        }
        assertFalse(Files.exists(changedRoot.toPath().resolve("skill-content/ab/content.bin")));

        service.deleteRecoverable(handle);
        service.deleteRecoverable(handle);
        assertFalse(service.existsRecoverable(handle));
    }

    /**
     * 验证崩溃窗口遗留的确定性 part 文件可由同一个句柄精确回收。
     *
     * @throws Exception 测试目录或反射配置失败
     */
    @Test
    public void deleteRecoverableRemovesFinalAndCrashLeftPartFile() throws Exception {
        File root = temporaryFolder.newFolder("crash-root");
        LocalFileStorageServiceImpl service = createService(root, "");
        FileStorageWriteHandle handle = service.prepareRecoverableWrite("skill-content/cd", "content.bin");
        Path target = Path.of(handle.getBasePath()).resolve(handle.getPath()).resolve(handle.getFilename());
        Files.createDirectories(target.getParent());
        Files.writeString(target, "final");
        Path part = service.recoverablePartPath(target, handle);
        Files.writeString(part, "partial");

        service.deleteRecoverable(handle);

        assertFalse(Files.exists(target));
        assertFalse(Files.exists(part));
    }

    /**
     * 验证旧版路径删除在文件已不存在时仍可幂等成功。
     *
     * @throws Exception 测试目录或反射配置失败
     */
    @Test
    public void deleteMissingLegacyPathIsIdempotent() throws Exception {
        File root = temporaryFolder.newFolder("legacy-delete-root");
        LocalFileStorageServiceImpl service = createService(root, "/files");

        service.delete("/files/missing.bin");
        service.delete("/files/missing.bin");

        assertFalse(Files.exists(root.toPath().resolve("missing.bin")));
    }

    /**
     * 验证句柄路径中的符号链接不会被跟随到存储根目录外。
     *
     * @throws Exception 测试目录、符号链接或反射配置失败
     */
    @Test
    public void recoverableWriteRejectsSymbolicLinkEscape() throws Exception {
        File root = temporaryFolder.newFolder("symlink-root");
        File outside = temporaryFolder.newFolder("outside");
        Files.createSymbolicLink(root.toPath().resolve("escape"), outside.toPath());
        LocalFileStorageServiceImpl service = createService(root, "");
        FileStorageWriteHandle handle = service.prepareRecoverableWrite("escape", "content.bin");

        assertThrows(IllegalStateException.class,
                () -> service.saveRecoverable(new BytesMultipartFile(new byte[]{1}), handle));
        assertFalse(Files.exists(outside.toPath().resolve("content.bin")));
    }

    /**
     * 创建具有测试根目录与 URL 前缀的服务。
     *
     * @param root 本地根目录
     * @param prefix URL 前缀
     * @return 本地存储服务
     * @throws Exception 反射设置字段失败
     */
    private LocalFileStorageServiceImpl createService(File root, String prefix) throws Exception {
        LocalFileStorageServiceImpl service = new LocalFileStorageServiceImpl();
        setField(service, "root", root.getAbsolutePath());
        setField(service, "prefix", prefix);
        return service;
    }

    /**
     * 设置服务私有配置字段。
     *
     * @param target 目标服务
     * @param name 字段名
     * @param value 字段值
     * @throws Exception 字段不存在或不可写时抛出
     */
    private void setField(Object target, String name, Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(target, value);
    }

    /**
     * 基于内存字节的 MultipartFile 测试替身。
     */
    private static final class BytesMultipartFile implements MultipartFile {
        /** 文件内容。 */
        private final byte[] bytes;

        /**
         * 创建测试上传文件。
         *
         * @param bytes 文件内容
         */
        private BytesMultipartFile(byte[] bytes) {
            this.bytes = bytes.clone();
        }

        /** {@inheritDoc} */
        @Override public String getName() { return "file"; }
        /** {@inheritDoc} */
        @Override public String getOriginalFilename() { return "content.bin"; }
        /** {@inheritDoc} */
        @Override public String getContentType() { return "application/octet-stream"; }
        /** {@inheritDoc} */
        @Override public boolean isEmpty() { return bytes.length == 0; }
        /** {@inheritDoc} */
        @Override public long getSize() { return bytes.length; }
        /** {@inheritDoc} */
        @Override public byte[] getBytes() { return bytes.clone(); }
        /** {@inheritDoc} */
        @Override public InputStream getInputStream() { return new ByteArrayInputStream(bytes); }
        /** {@inheritDoc} */
        @Override public void transferTo(File dest) throws IOException { Files.write(dest.toPath(), bytes); }
    }
}
