package tech.easyflow.common.filestorage.impl;

import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.MinioClient;
import okhttp3.Headers;
import org.junit.Test;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.UploadPretreatment;
import org.dromara.x.file.storage.core.platform.FileStorage;
import org.dromara.x.file.storage.core.platform.FileStorageClientFactory;
import org.dromara.x.file.storage.core.platform.MinioFileStorage;
import org.dromara.x.file.storage.core.recorder.FileRecorder;
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.util.function.Consumer;

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

/**
 * {@link XFIleStorageServiceImpl} 删除结果传播测试。
 */
public class XFIleStorageServiceImplTest {

    /**
     * 验证底层明确返回 false 时抛出带有效消息的异常。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void deleteFalseThrowsNonEmptyException() throws Exception {
        DeleteResultStorageService delegate = new DeleteResultStorageService(false, true, false);
        XFIleStorageServiceImpl service = createService(delegate);

        RuntimeException exception = assertThrows(
                RuntimeException.class, () -> service.delete("skill-content/retry.bin"));

        assertFalse(exception.getMessage() == null || exception.getMessage().isBlank());
        assertEquals("skill-content/retry.bin", delegate.getLastPath());
    }

    /**
     * 验证底层确认删除成功时正常返回。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void deleteTrueReturnsNormally() throws Exception {
        DeleteResultStorageService delegate = new DeleteResultStorageService(true, false, false);
        XFIleStorageServiceImpl service = createService(delegate);

        service.delete("skill-content/deleted.bin");

        assertEquals("skill-content/deleted.bin", delegate.getLastPath());
    }

    /**
     * 验证物理文件已不存在时会清理残留记录并按幂等成功返回。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void deleteAbsentFileCleansResidualRecord() throws Exception {
        DeleteResultStorageService delegate = new DeleteResultStorageService(false, false, true);
        XFIleStorageServiceImpl service = createService(delegate);

        service.delete("skill-content/already-absent.bin");

        assertEquals("skill-content/already-absent.bin", delegate.getLastPath());
        assertFalse(delegate.hasRecord());
    }

    /**
     * 验证 prepare 与 save 固定平台、基础路径、相对路径及文件名，并同时返回 URL 与 locator。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverableSaveUsesExactPreparedLocation() throws Exception {
        RecoverablePlatform platform = new RecoverablePlatform("minio-main", "attachment", "https://files/");
        RecoverableStorageService delegate = new RecoverableStorageService(platform);
        XFIleStorageServiceImpl service = createService(delegate);
        FileStorageWriteHandle handle = service.prepareRecoverableWrite("skill-content/ab", "content.bin");

        FileStorageWriteResult result = service.saveRecoverable(
                new BytesMultipartFile("content".getBytes(java.nio.charset.StandardCharsets.UTF_8)), handle);

        assertEquals("xFileStorage", handle.getBackend());
        assertEquals("minio-main", handle.getPlatform());
        assertEquals("attachment", handle.getBasePath());
        assertEquals("/skill-content/ab/", delegate.uploadPath);
        assertEquals("content.bin", delegate.uploadFilename);
        assertEquals("minio-main", delegate.uploadPlatform);
        assertEquals("https://files/attachment/skill-content/ab/content.bin", result.getUrl());
        assertEquals(handle, FileStorageWriteHandle.decodeLocator(result.getLocator()));
        assertTrue(platform.exists);
    }

    /**
     * 验证可恢复上传保留上游已经归一化的 MIME，不再按 txt 扩展名二次覆盖。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverableSavePreservesNormalizedTextContentType()
            throws Exception {
        RecoverablePlatform platform = new RecoverablePlatform(
                "minio-main",
                "attachment",
                "https://files/");
        RecoverableStorageService delegate =
                new RecoverableStorageService(platform);
        XFIleStorageServiceImpl service = createService(delegate);
        FileStorageWriteHandle handle = service.prepareRecoverableWrite(
                "workflow-api-upload/upload-1",
                "content.txt");

        service.saveRecoverable(
                new BytesMultipartFile(
                        "content".getBytes(
                                java.nio.charset.StandardCharsets.UTF_8),
                        "content.txt",
                        "application/octet-stream"),
                handle);

        assertEquals(
                "application/octet-stream",
                delegate.uploadContentType);
    }

    /**
     * 验证客户端未声明 MIME 时继续为 txt 文件补充 UTF-8 文本类型。
     */
    @Test
    public void textFileWithoutContentTypeUsesUtf8Fallback() {
        BytesMultipartFile file = new BytesMultipartFile(
                new byte[]{1},
                "content.TXT",
                null);

        assertEquals(
                "text/plain; charset=utf-8",
                XFIleStorageServiceImpl.getFileContentType(file));
    }

    /**
     * 验证 MinIO 可恢复读取使用已配置客户端和句柄中的精确对象键，不请求公开 URL。
     *
     * @throws Exception 测试替身配置或流读取失败
     */
    @Test
    public void recoverableReadUsesMinioClientAndExactObjectKey() throws Exception {
        byte[] content = "managed-content".getBytes(java.nio.charset.StandardCharsets.UTF_8);
        RecordingMinioClient client = new RecordingMinioClient(content);
        MinioFileStorage platform = new MinioFileStorage();
        platform.setPlatform("minio-main");
        platform.setBucketName("easyflow");
        platform.setBasePath("easyflow/");
        platform.setDomain("http://127.0.0.1:39000/");
        platform.setClientFactory(new FixedMinioClientFactory(client));
        XFIleStorageServiceImpl service = createService(
                new RecoverableStorageService(platform));
        FileStorageWriteHandle handle = new FileStorageWriteHandle(
                "xFileStorage",
                "minio-main",
                "easyflow/",
                "workflow-api-upload/request",
                "content.bin");

        byte[] actual;
        try (InputStream inputStream = service.readRecoverable(handle)) {
            actual = inputStream.readAllBytes();
        }

        assertArrayEquals(content, actual);
        assertEquals("easyflow", client.lastArgs.bucket());
        assertEquals(
                "easyflow/workflow-api-upload/request/content.bin",
                client.lastArgs.object());
    }

    /**
     * 验证 recorder 完全缺失目标记录时，精确删除仍直接作用于物理平台并成功。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverableDeleteWithoutRecorderEntryStillDeletesPhysicalObject() throws Exception {
        RecoverablePlatform platform = new RecoverablePlatform("minio-main", "easyflow/", "https://files/");
        platform.exists = true;
        RecoverableStorageService delegate = new RecoverableStorageService(platform);
        XFIleStorageServiceImpl service = createService(delegate);
        FileStorageWriteHandle handle = new FileStorageWriteHandle(
                "xFileStorage", "minio-main", "easyflow/", "skill-content/ab", "content.bin");

        service.deleteRecoverable(handle);

        assertFalse(platform.exists);
        assertEquals(1, platform.deleteCalls);
        assertEquals(1, delegate.recorderDeleteCalls);
    }

    /**
     * 验证具体平台报告删除失败且物理对象仍存在时必须抛出异常。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverableDeleteFailureIsNotMaskedByRecorder() throws Exception {
        RecoverablePlatform platform = new RecoverablePlatform("minio-main", "easyflow/", "https://files/");
        platform.exists = true;
        platform.deleteSucceeds = false;
        RecoverableStorageService delegate = new RecoverableStorageService(platform);
        XFIleStorageServiceImpl service = createService(delegate);
        FileStorageWriteHandle handle = new FileStorageWriteHandle(
                "xFileStorage", "minio-main", "easyflow/", "skill-content/ab", "content.bin");

        IllegalStateException exception = assertThrows(
                IllegalStateException.class, () -> service.deleteRecoverable(handle));

        assertTrue(exception.getMessage().contains("仍存在"));
        assertTrue(platform.exists);
        assertEquals(0, delegate.recorderDeleteCalls);
    }

    /**
     * 验证物理删除确认成功后，recorder 清理异常不会反向伪造物理失败。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverableDeleteIgnoresRecorderCleanupFailureAfterPhysicalSuccess() throws Exception {
        RecoverablePlatform platform = new RecoverablePlatform("minio-main", "attachment", "https://files/");
        platform.exists = true;
        RecoverableStorageService delegate = new RecoverableStorageService(platform);
        delegate.recorderDeleteThrows = true;
        XFIleStorageServiceImpl service = createService(delegate);
        FileStorageWriteHandle handle = new FileStorageWriteHandle(
                "xFileStorage", "minio-main", "attachment", "skill-content/ab", "content.bin");

        service.deleteRecoverable(handle);

        assertFalse(platform.exists);
        assertEquals(1, delegate.recorderDeleteCalls);
    }

    /**
     * 验证平台默认 basePath 切换后，支持 FileInfo.basePath 的对象存储仍按历史句柄删除旧对象。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverableDeleteUsesPersistedBasePathAfterConfigurationSwitch() throws Exception {
        RecoverablePlatform platform = new RecoverablePlatform("minio-main", "old-root", "https://files/");
        RecoverableStorageService delegate = new RecoverableStorageService(platform);
        XFIleStorageServiceImpl service = createService(delegate);
        FileStorageWriteHandle handle = service.prepareRecoverableWrite("skill-content/ab", "content.bin");
        platform.basePath = "new-root";
        platform.exists = true;

        service.deleteRecoverable(handle);

        assertEquals("old-root/skill-content/ab/content.bin", platform.lastDeletedKey);
        assertFalse(platform.exists);
    }

    /**
     * 验证上传前 basePath 已切换时 fail-fast，避免把预留 locator 写向新目录。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverableSaveRejectsBasePathSwitchBeforeUpload() throws Exception {
        RecoverablePlatform platform = new RecoverablePlatform("minio-main", "old-root", "https://files/");
        RecoverableStorageService delegate = new RecoverableStorageService(platform);
        XFIleStorageServiceImpl service = createService(delegate);
        FileStorageWriteHandle handle = service.prepareRecoverableWrite("skill-content/ab", "content.bin");
        platform.basePath = "new-root";

        IllegalStateException exception = assertThrows(IllegalStateException.class,
                () -> service.saveRecoverable(new BytesMultipartFile(new byte[]{1}), handle));

        assertTrue(exception.getMessage().contains("基础路径已变化"));
        assertNull(delegate.uploadPlatform);
    }

    /**
     * 验证未公开 getBasePath 的 x-file-storage 平台在 prepare 阶段立即失败。
     *
     * @throws Exception 注入测试替身失败
     */
    @Test
    public void recoverablePrepareFailsWhenPlatformDoesNotExposeBasePath() throws Exception {
        RecoverableStorageService delegate = new RecoverableStorageService(new NoBasePathPlatform("custom"));
        XFIleStorageServiceImpl service = createService(delegate);

        IllegalStateException exception = assertThrows(
                IllegalStateException.class,
                () -> service.prepareRecoverableWrite("skill-content", "content.bin"));

        assertTrue(exception.getMessage().contains("getBasePath"));
    }

    /**
     * 创建注入指定底层存储替身的服务。
     *
     * @param delegate 底层存储替身
     * @return 待测试服务
     * @throws Exception 反射注入失败
     */
    private XFIleStorageServiceImpl createService(
            org.dromara.x.file.storage.core.FileStorageService delegate) throws Exception {
        XFIleStorageServiceImpl service = new XFIleStorageServiceImpl();
        Field field = XFIleStorageServiceImpl.class.getDeclaredField("fileStorageService");
        field.setAccessible(true);
        field.set(service, delegate);
        return service;
    }

    /**
     * 支持精确物理操作的 x-file-storage 平台测试替身。
     */
    public static final class RecoverablePlatform implements FileStorage {
        /** 平台名称。 */
        private String platform;
        /** 基础路径。 */
        private String basePath;
        /** URL 域名前缀。 */
        private final String domain;
        /** 物理存在状态。 */
        private boolean exists;
        /** 删除是否成功。 */
        private boolean deleteSucceeds = true;
        /** 删除调用次数。 */
        private int deleteCalls;
        /** 最后删除的完整对象 key。 */
        private String lastDeletedKey;

        /**
         * 创建平台替身。
         *
         * @param platform 平台名
         * @param basePath 基础路径
         * @param domain URL 域名前缀
         */
        public RecoverablePlatform(String platform, String basePath, String domain) {
            this.platform = platform;
            this.basePath = basePath;
            this.domain = domain;
        }

        /**
         * 获取公开基础路径。
         *
         * @return 基础路径
         */
        public String getBasePath() { return basePath; }

        /**
         * 获取公开 URL 域名前缀。
         *
         * @return 域名前缀
         */
        public String getDomain() { return domain; }

        /** {@inheritDoc} */
        @Override public String getPlatform() { return platform; }
        /** {@inheritDoc} */
        @Override public void setPlatform(String platform) { this.platform = platform; }
        /** {@inheritDoc} */
        @Override public boolean save(FileInfo fileInfo, UploadPretreatment pre) { exists = true; return true; }

        /** {@inheritDoc} */
        @Override
        public boolean delete(FileInfo fileInfo) {
            deleteCalls++;
            lastDeletedKey = getFileKey(fileInfo);
            if (deleteSucceeds) {
                exists = false;
            }
            return deleteSucceeds;
        }

        /** {@inheritDoc} */
        @Override public boolean exists(FileInfo fileInfo) { return exists; }
        /** {@inheritDoc} */
        @Override public void download(FileInfo fileInfo, Consumer<InputStream> consumer) { }
        /** {@inheritDoc} */
        @Override public void downloadTh(FileInfo fileInfo, Consumer<InputStream> consumer) { }
    }

    /**
     * 不公开基础路径的平台替身。
     */
    private static final class NoBasePathPlatform implements FileStorage {
        /** 平台名。 */
        private String platform;

        /**
         * 创建平台替身。
         *
         * @param platform 平台名
         */
        private NoBasePathPlatform(String platform) { this.platform = platform; }

        /** {@inheritDoc} */
        @Override public String getPlatform() { return platform; }
        /** {@inheritDoc} */
        @Override public void setPlatform(String platform) { this.platform = platform; }
        /** {@inheritDoc} */
        @Override public boolean save(FileInfo fileInfo, UploadPretreatment pre) { return true; }
        /** {@inheritDoc} */
        @Override public boolean delete(FileInfo fileInfo) { return true; }
        /** {@inheritDoc} */
        @Override public boolean exists(FileInfo fileInfo) { return false; }
        /** {@inheritDoc} */
        @Override public void download(FileInfo fileInfo, Consumer<InputStream> consumer) { }
        /** {@inheritDoc} */
        @Override public void downloadTh(FileInfo fileInfo, Consumer<InputStream> consumer) { }
    }

    /**
     * 可捕获固定上传参数并提供具体平台的聚合服务替身。
     */
    private static final class RecoverableStorageService
            extends org.dromara.x.file.storage.core.FileStorageService {
        /** 具体平台。 */
        private final FileStorage platform;
        /** 上传平台。 */
        private String uploadPlatform;
        /** 上传路径。 */
        private String uploadPath;
        /** 上传文件名。 */
        private String uploadFilename;
        /** 上传媒体类型。 */
        private String uploadContentType;
        /** recorder 删除调用次数。 */
        private int recorderDeleteCalls;
        /** recorder 删除是否抛出异常。 */
        private boolean recorderDeleteThrows;

        /**
         * 创建聚合服务替身。
         *
         * @param platform 具体平台
         */
        private RecoverableStorageService(FileStorage platform) {
            this.platform = platform;
            setFileRecorder(new FileRecorder() {
                @Override public boolean save(FileInfo fileInfo) { return true; }
                @Override public void update(FileInfo fileInfo) { }
                @Override public FileInfo getByUrl(String url) { return null; }
                @Override public boolean delete(String url) {
                    recorderDeleteCalls++;
                    if (recorderDeleteThrows) {
                        throw new IllegalStateException("recorder unavailable");
                    }
                    return false;
                }
                @Override public void saveFilePart(org.dromara.x.file.storage.core.upload.FilePartInfo filePartInfo) { }
                @Override public void deleteFilePartByUploadId(String uploadId) { }
            });
        }

        /** {@inheritDoc} */
        @SuppressWarnings("unchecked")
        @Override public <T extends FileStorage> T getFileStorage() { return (T) platform; }

        /** {@inheritDoc} */
        @SuppressWarnings("unchecked")
        @Override
        public <T extends FileStorage> T getFileStorage(String name) {
            return platform.getPlatform().equals(name) ? (T) platform : null;
        }

        /** {@inheritDoc} */
        @Override
        public org.dromara.x.file.storage.core.upload.UploadPretreatment of(Object file) {
            return new CapturingUploadPretreatment(this);
        }
    }

    /**
     * 始终返回同一 MinIO 客户端的测试工厂。
     */
    private static final class FixedMinioClientFactory
            implements FileStorageClientFactory<MinioClient> {

        /** 固定客户端。 */
        private final MinioClient client;

        /**
         * 创建固定客户端工厂。
         *
         * @param client MinIO 客户端
         */
        private FixedMinioClientFactory(MinioClient client) {
            this.client = client;
        }

        /** {@inheritDoc} */
        @Override
        public String getPlatform() {
            return "minio-main";
        }

        /** {@inheritDoc} */
        @Override
        public MinioClient getClient() {
            return client;
        }
    }

    /**
     * 记录精确对象参数并返回内存内容的 MinIO 客户端替身。
     */
    private static final class RecordingMinioClient extends MinioClient {

        /** 固定返回内容。 */
        private final byte[] content;
        /** 最后一次读取参数。 */
        private GetObjectArgs lastArgs;

        /**
         * 创建内存 MinIO 客户端替身。
         *
         * @param content 固定返回内容
         */
        private RecordingMinioClient(byte[] content) {
            super(MinioClient.builder()
                    .endpoint("http://127.0.0.1:39000")
                    .credentials("test-access-key", "test-secret-key")
                    .build());
            this.content = content.clone();
        }

        /** {@inheritDoc} */
        @Override
        public GetObjectResponse getObject(GetObjectArgs args) {
            this.lastArgs = args;
            return new GetObjectResponse(
                    new Headers.Builder().build(),
                    args.bucket(),
                    null,
                    args.object(),
                    new ByteArrayInputStream(content));
        }
    }

    /**
     * 不访问真实网络、仅捕获上传参数的预处理器。
     */
    private static final class CapturingUploadPretreatment
            extends org.dromara.x.file.storage.core.upload.UploadPretreatment {
        /** 所属聚合服务替身。 */
        private final RecoverableStorageService delegate;

        /**
         * 创建捕获预处理器。
         *
         * @param delegate 聚合服务替身
         */
        private CapturingUploadPretreatment(RecoverableStorageService delegate) {
            this.delegate = delegate;
        }

        /**
         * 测试替身不创建 FileWrapper，仅保持生产链式调用兼容。
         *
         * @param contentType 文件媒体类型
         * @return 当前预处理器
         */
        @Override
        public org.dromara.x.file.storage.core.upload.UploadPretreatment setContentType(String contentType) {
            delegate.uploadContentType = contentType;
            return this;
        }

        /** {@inheritDoc} */
        @Override
        public FileInfo upload() {
            delegate.uploadPlatform = getPlatform();
            delegate.uploadPath = getPath();
            delegate.uploadFilename = getSaveFilename();
            RecoverablePlatform platform = (RecoverablePlatform) delegate.platform;
            platform.exists = true;
            return new FileInfo()
                    .setPlatform(getPlatform())
                    .setBasePath(platform.getBasePath())
                    .setPath(getPath())
                    .setFilename(getSaveFilename())
                    .setUrl(platform.getDomain() + platform.getBasePath() + getPath() + getSaveFilename());
        }
    }

    /**
     * 基于字节数组的 MultipartFile 测试替身。
     */
    private static final class BytesMultipartFile implements MultipartFile {
        /** 文件内容。 */
        private final byte[] bytes;
        /** 文件名。 */
        private final String filename;
        /** 文件媒体类型。 */
        private final String contentType;

        /**
         * 创建上传文件替身。
         *
         * @param bytes 文件内容
         */
        private BytesMultipartFile(byte[] bytes) {
            this(bytes, "content.bin", "application/octet-stream");
        }

        /**
         * 创建指定文件名和媒体类型的上传文件替身。
         *
         * @param bytes 文件内容
         * @param filename 文件名
         * @param contentType 文件媒体类型
         */
        private BytesMultipartFile(
                byte[] bytes,
                String filename,
                String contentType) {
            this.bytes = bytes.clone();
            this.filename = filename;
            this.contentType = contentType;
        }

        /** {@inheritDoc} */
        @Override public String getName() { return "file"; }
        /** {@inheritDoc} */
        @Override public String getOriginalFilename() { return filename; }
        /** {@inheritDoc} */
        @Override public String getContentType() { return contentType; }
        /** {@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); }
    }

    /**
     * 可控制删除结果的 x-file-storage 测试替身。
     */
    private static final class DeleteResultStorageService
            extends org.dromara.x.file.storage.core.FileStorageService {

        private final boolean deleteResult;
        private final boolean exists;
        private boolean recordExists;
        private String lastPath;

        /**
         * 创建测试替身。
         *
         * @param deleteResult 删除返回值
         * @param exists 物理文件是否存在
         * @param recordExists 是否存在文件记录
         */
        private DeleteResultStorageService(boolean deleteResult, boolean exists, boolean recordExists) {
            this.deleteResult = deleteResult;
            this.exists = exists;
            this.recordExists = recordExists;
            setFileRecorder(new FileRecorder() {
                @Override public boolean save(FileInfo fileInfo) { return true; }
                @Override public void update(FileInfo fileInfo) { }
                @Override public FileInfo getByUrl(String url) {
                    return DeleteResultStorageService.this.recordExists ? new FileInfo() : null;
                }
                @Override public boolean delete(String url) {
                    boolean previous = DeleteResultStorageService.this.recordExists;
                    DeleteResultStorageService.this.recordExists = false;
                    return previous;
                }
                @Override public void saveFilePart(org.dromara.x.file.storage.core.upload.FilePartInfo filePartInfo) { }
                @Override public void deleteFilePartByUploadId(String uploadId) { }
            });
        }

        /**
         * 返回预设删除结果并记录路径。
         *
         * @param path 删除路径
         * @return 预设结果
         */
        @Override
        public boolean delete(String path) {
            lastPath = path;
            return deleteResult;
        }

        /**
         * 返回预设物理存在状态。
         *
         * @param path 文件路径
         * @return 预设存在状态
         */
        @Override
        public boolean exists(String path) {
            return exists;
        }

        /**
         * 返回测试文件记录。
         *
         * @param url 文件 URL
         * @return 记录存在时返回 FileInfo
         */
        @Override
        public FileInfo getFileInfoByUrl(String url) {
            return recordExists ? new FileInfo() : null;
        }

        /**
         * 获取最后一次删除路径。
         *
         * @return 删除路径
         */
        private String getLastPath() {
            return lastPath;
        }

        /**
         * 判断测试文件记录是否仍存在。
         *
         * @return 存在时返回 true
         */
        private boolean hasRecord() {
            return recordExists;
        }
    }
}
