//@category REA Verification

import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;

import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.app.util.headless.HeadlessScript;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.Function;
import ghidra.program.model.symbol.SourceType;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.decompiler.DecompileProcess.DisposeState;
import ghidra.util.task.TaskMonitor;

/** Real source-identity cases unavailable through ordinary single-file CLI imports. */
public final class ReaFileOffsetSourceProbe extends HeadlessScript {
    @Override
    public void run() throws Exception {
        var file = GhidraScriptUtil.findScriptByName("ReaGhidraBridge.java");
        var bridge = GhidraScriptUtil.getProvider(file).getScriptInstance(file, new PrintWriter(System.out, true));
        bridge.set(state, monitor, new PrintWriter(System.out, true));
        var space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace");
        space.setAccessible(true);
        space.set(null, currentProgram.getAddressFactory().getDefaultAddressSpace());
        var identity = bridge.getClass().getDeclaredField("admittedTargetSha256");
        identity.setAccessible(true);
        identity.set(bridge, currentProgram.getExecutableSHA256().toLowerCase(Locale.ROOT));
        var resolve = bridge.getClass().getDeclaredMethod("resolveDecompileProcedure", String.class, JsonObject.class);
        resolve.setAccessible(true);
        var reverse = bridge.getClass().getDeclaredMethod("addressToFileOffset", JsonObject.class);
        reverse.setAccessible(true);

        var memory = currentProgram.getMemory();
        var original = memory.getAllFileBytes().get(0);
        byte[] bytes = new byte[Math.toIntExact(original.getSize())];
        require(original.getOriginalBytes(0, bytes) == bytes.length, "complete fixture source");
        byte[] auxiliary = new byte[0x1000];
        auxiliary[0x910] = (byte)0xc3;
        var foreign = memory.createFileBytes("auxiliary.bin", 0, auxiliary.length,
            new ByteArrayInputStream(auxiliary), monitor);
        var address = toAddr(0x140100000L);
        memory.createInitializedBlock("auxiliary", address, foreign, 0, auxiliary.length, false);
        var entry = address.add(0x910);
        currentProgram.getFunctionManager().createFunction("foreign_only", entry,
            new AddressSet(entry), SourceType.USER_DEFINED);
        rejected(resolve, bridge, "no loaded source mapping", "file-offset:0x910", new JsonObject());
        rejected(reverse, bridge, "0 authoritative target-file mappings", reverseInput("0x140100910"));

        // A source object's declared original-file start does not make its local
        // offsets a complete, digest-matched target representation.
        var partial = memory.createFileBytes("partial.bin", 0x200, 0x200,
            new ByteArrayInputStream(bytes, 0x200, 0x200), monitor);
        memory.createInitializedBlock("partial", toAddr(0x140300000L), partial, 0, 0x200, false);
        rejected(reverse, bridge, "0 authoritative target-file mappings", reverseInput("0x140300010"));
        var selected = (Function)resolve.invoke(bridge, "file-offset:0x210", new JsonObject());
        require(selected.getEntryPoint().equals(toAddr(0x140001010L)), "original target selected despite other sources");
        var mapped = (JsonObject)reverse.invoke(bridge, reverseInput("0x140001010"));
        require(mapped.get("file_offset").getAsLong() == 0x210, "original target round trip");

        // Inject backend failures after real source/function resolution. This
        // verifies context propagation, not a naturally failing decompiler.
        var pseudocode = bridge.getClass().getDeclaredMethod("procedurePseudocode", JsonObject.class);
        pseudocode.setAccessible(true);
        var decompiler = bridge.getClass().getDeclaredField("decompiler");
        decompiler.setAccessible(true);
        JsonObject request = new JsonObject();
        request.add("document", JsonNull.INSTANCE);
        request.addProperty("procedure", "file-offset:0x210");
        for (DisposeState disposition : new DisposeState[] { DisposeState.NOT_DISPOSED, DisposeState.DISPOSED_ON_CANCEL }) {
            decompiler.set(bridge, new DecompInterface() {
                @Override
                public DecompileResults decompileFunction(Function function, int timeout, TaskMonitor taskMonitor) {
                    return new DecompileResults(function, currentProgram.getLanguage(),
                        currentProgram.getCompilerSpec(), null, "Injected backend failure", null, disposition);
                }
            });
            Throwable failure = rejected(pseudocode, bridge,
                "Selector file-offset:0x210 mapped to analysis address 0x140001010 and procedure 0x140001010", request);
            var code = failure.getClass().getDeclaredField("code");
            code.setAccessible(true);
            require(code.get(failure).equals(disposition == DisposeState.DISPOSED_ON_CANCEL
                ? "decompile_cancelled" : "decompile_failed"), "backend failure code retained");
        }

        var duplicate = memory.createFileBytes("duplicate.bin", 0, bytes.length,
            new ByteArrayInputStream(bytes), monitor);
        memory.createInitializedBlock("duplicate", toAddr(0x140201000L), duplicate, 0x200, 0x200, false);
        rejected(resolve, bridge, "multiple loaded mappings", "file-offset:0x210", new JsonObject());
        println("REA_FILE_OFFSET_SOURCE_OK foreign/partial rejected; original round trip; duplicate ambiguity; injected backend failure context retained");
    }

    private JsonObject reverseInput(String address) {
        JsonObject params = new JsonObject();
        params.add("document", JsonNull.INSTANCE);
        params.addProperty("address", address);
        return params;
    }

    private Throwable rejected(Method method, Object bridge, String reason, Object... arguments) throws Exception {
        try {
            method.invoke(bridge, arguments);
        } catch (InvocationTargetException rejected) {
            require(rejected.getCause().getMessage().contains(reason), "expected mapping failure: " + reason);
            return rejected.getCause();
        }
        throw new AssertionError("Expected mapping failure: " + reason);
    }

    private void require(boolean condition, String message) {
        if (!condition) throw new AssertionError(message);
    }
}
