package com.easyagents.agent.runtime.tool.operate;

import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.tool.AgentTool;
import io.agentscope.core.tool.ToolCallParam;
import org.junit.Assert;
import org.junit.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 测试安全读写工具与工作区配额。
 */
public class WorkspaceFileToolsTest {

    @Test
    public void shouldWriteAtomicallyAndReadOnlyRequestedRange() throws IOException {
        Fixture fixture = fixture(1024, 1024, 10, 1024);
        AgentTool write = new SafeWriteFileTool(fixture.pathGuard(), fixture.quotaGuard()).writeTextFileTool();
        AgentTool read = new SafeReadFileTool(fixture.pathGuard(), fixture.quotaGuard()).viewTextFileTool();

        ToolResultBlock writeResult = call(write, Map.of(
                "file_path", "notes/example.txt",
                "content", "first\nsecond\nthird\nfourth\n"));
        ToolResultBlock readResult = call(read, Map.of(
                "file_path", "notes/example.txt",
                "ranges", "2,3"));

        Assert.assertTrue(text(writeResult).contains("successfully"));
        Assert.assertTrue(text(readResult).contains("2: second"));
        Assert.assertTrue(text(readResult).contains("3: third"));
        Assert.assertFalse(text(readResult).contains("1: first"));
        Assert.assertFalse(text(readResult).contains(fixture.root().toString()));
        try (var files = Files.list(fixture.root().resolve("notes"))) {
            Assert.assertTrue(files.noneMatch(path -> path.getFileName().toString().startsWith(".easyagents-write-")));
        }
    }

    @Test
    public void shouldRejectWriteBeyondQuotaWithoutPartialFile() throws IOException {
        Fixture fixture = fixture(5, 5, 1, 5);
        AgentTool write = new SafeWriteFileTool(fixture.pathGuard(), fixture.quotaGuard()).writeTextFileTool();

        ToolResultBlock result = call(write, Map.of("file_path", "too-large.txt", "content", "123456"));

        Assert.assertTrue(text(result).contains("WORKSPACE_QUOTA_EXCEEDED"));
        Assert.assertFalse(Files.exists(fixture.root().resolve("too-large.txt")));
    }

    @Test
    public void shouldCountCreatedDirectoriesAgainstEntryQuota() throws IOException {
        Fixture fixture = fixture(1024, 1024, 1, 1024);
        AgentTool write = new SafeWriteFileTool(fixture.pathGuard(), fixture.quotaGuard()).writeTextFileTool();

        String output = text(call(write, Map.of("file_path", "nested/value.txt", "content", "ok")));

        Assert.assertTrue(output, output.contains("WORKSPACE_QUOTA_EXCEEDED"));
        Assert.assertFalse(Files.exists(fixture.root().resolve("nested")));
    }

    @Test
    public void shouldInvokeConfiguredQuotaHook() throws IOException {
        Path root = Files.createTempDirectory("workspace-hook-").toAbsolutePath();
        WorkspacePathGuard pathGuard = new WorkspacePathGuard(root);
        AtomicInteger writes = new AtomicInteger();
        WorkspaceQuotaHook hook = new WorkspaceQuotaHook() {
            @Override
            public void beforeRead(Path workspaceRoot, Path target, long requestedBytes) {
            }

            @Override
            public void beforeWrite(Path workspaceRoot, Path target, long previousBytes, long resultingBytes) {
                writes.incrementAndGet();
            }
        };
        WorkspaceQuotaGuard quotaGuard = new WorkspaceQuotaGuard(
                pathGuard, new WorkspaceQuotaLimits(1024, 1024, 10, 1024), hook);
        AgentTool write = new SafeWriteFileTool(pathGuard, quotaGuard).writeTextFileTool();

        call(write, Map.of("file_path", "hook.txt", "content", "ok"));

        Assert.assertEquals(1, writes.get());
    }

    @Test
    public void shouldRejectDirectoryListingWhenWorkspaceContainsSymlink() throws IOException {
        Fixture fixture = fixture(1024, 1024, 10, 1024);
        Files.writeString(fixture.root().resolve("safe.txt"), "safe");
        Path outside = Files.createTempFile("workspace-list-outside-", ".txt");
        try {
            Files.createSymbolicLink(fixture.root().resolve("blocked"), outside);
        } catch (UnsupportedOperationException error) {
            return;
        }
        AgentTool list = new SafeReadFileTool(fixture.pathGuard(), fixture.quotaGuard()).listDirectoryTool();

        String output = text(call(list, Map.of("dir_path", ".")));

        Assert.assertTrue(output, output.contains("WORKSPACE_PATH_INVALID"));
        Assert.assertFalse(output.contains(fixture.root().toString()));
        Assert.assertFalse(output.contains(outside.toString()));
    }

    @Test
    public void shouldReadSmallRangeFromFileLargerThanFullReadLimit() throws IOException {
        Fixture fixture = fixture(64 * 1024, 64 * 1024, 10, 16);
        Files.writeString(fixture.root().resolve("large.txt"), "first\n" + "x".repeat(4096) + "\n");
        AgentTool read = new SafeReadFileTool(fixture.pathGuard(), fixture.quotaGuard()).viewTextFileTool();

        String output = text(call(read, Map.of("file_path", "large.txt", "ranges", "1,1")));

        Assert.assertTrue(output, output.contains("1: first"));
        Assert.assertFalse(output.contains("WORKSPACE_QUOTA_EXCEEDED"));
    }

    @Test
    public void shouldBoundDirectoryListingAndReportTruncation() throws IOException {
        Fixture fixture = fixture(64 * 1024, 64 * 1024, 0, 1024);
        for (int index = 0; index < 1001; index++) {
            Files.writeString(fixture.root().resolve("entry-" + index + ".txt"), "x");
        }
        AgentTool list = new SafeReadFileTool(fixture.pathGuard(), fixture.quotaGuard()).listDirectoryTool();

        String output = text(call(list, Map.of("dir_path", ".")));

        Assert.assertTrue(output, output.contains("Truncated: true; limit=1000"));
        Assert.assertEquals(1000, output.lines().filter(line -> line.startsWith("file\t")).count());
    }

    @Test
    public void shouldRejectDirectoryListingBeforeSortingOverQuotaWorkspace() throws IOException {
        Fixture fixture = fixture(64 * 1024, 64 * 1024, 3, 1024);
        for (int index = 0; index < 4; index++) {
            Files.createDirectory(fixture.root().resolve("directory-" + index));
        }
        AgentTool list = new SafeReadFileTool(fixture.pathGuard(), fixture.quotaGuard()).listDirectoryTool();

        String output = text(call(list, Map.of("dir_path", ".")));

        Assert.assertTrue(output, output.contains("WORKSPACE_QUOTA_EXCEEDED"));
    }

    @Test
    public void shouldReturnStableReadErrorCodes() throws IOException {
        Fixture fixture = fixture(1024, 1024, 10, 1024);
        Files.createDirectory(fixture.root().resolve("folder"));
        Files.writeString(fixture.root().resolve("valid.txt"), "ok\n");
        AgentTool read = new SafeReadFileTool(fixture.pathGuard(), fixture.quotaGuard()).viewTextFileTool();

        Assert.assertTrue(text(call(read, Map.of("file_path", "missing.txt"))).contains("FILE_NOT_FOUND"));
        Assert.assertTrue(text(call(read, Map.of("file_path", "folder"))).contains("FILE_TYPE_INVALID"));
        Assert.assertTrue(text(call(read, Map.of("file_path", "../outside")))
                .contains("WORKSPACE_PATH_INVALID"));
        Assert.assertTrue(text(call(read, Map.of("file_path", "valid.txt", "ranges", "x,y")))
                .contains("INVALID_ARGUMENT"));
    }

    private Fixture fixture(long total, long single, long count, long read) throws IOException {
        Path root = Files.createTempDirectory("workspace-file-tool-").toAbsolutePath();
        WorkspacePathGuard pathGuard = new WorkspacePathGuard(root);
        WorkspaceQuotaGuard quotaGuard = new WorkspaceQuotaGuard(
                pathGuard, new WorkspaceQuotaLimits(total, single, count, read), WorkspaceQuotaHook.noop());
        return new Fixture(root, pathGuard, quotaGuard);
    }

    private ToolResultBlock call(AgentTool tool, Map<String, Object> input) {
        return tool.callAsync(ToolCallParam.builder().input(input).build()).block();
    }

    private String text(ToolResultBlock result) {
        return ((TextBlock) result.getOutput().get(0)).getText();
    }

    /**
     * 测试工具夹具。
     *
     * @param root 工作区根
     * @param pathGuard 路径保护器
     * @param quotaGuard 配额保护器
     */
    private record Fixture(Path root, WorkspacePathGuard pathGuard, WorkspaceQuotaGuard quotaGuard) {
    }
}
