package ai.decompile.engine.service;

import static org.junit.jupiter.api.Assertions.*;

import ai.decompile.engine.model.AnalysisResult;
import org.junit.jupiter.api.Test;

class AbstractIdaEngineServiceTest {

  @Test
  void shouldParseStringsWhenPresent() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "strings": [
            {"address": "0x00403000", "value": "Hello World", "encoding": "ASCSTR_C", "referencedBy": "sub_1000 sub_2500"},
            {"address": "0x00403010", "value": "path/to/file", "encoding": "ASCSTR_C", "referencedBy": ""}
          ],
          "imports": [],
          "functions": []
        }
        """;

    var result = invokeParseOutput(json);

    assertNotNull(result);
    assertEquals(2, result.strings().size());
    assertEquals("sub_1000 sub_2500", result.strings().get(0).referencedBy());
    assertEquals("", result.strings().get(1).referencedBy());
  }

  @Test
  void shouldReturnEmptyStringsWhenBlockAbsent() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "functions": []
        }
        """;

    var result = invokeParseOutput(json);

    assertNotNull(result);
    assertTrue(result.strings().isEmpty());
    assertTrue(result.imports().isEmpty());
  }

  @Test
  void shouldParseImports() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "strings": [],
          "imports": [
            {"dll": "KERNEL32", "name": "CreateFileA", "address": "0x00405000", "ordinal": 0},
            {"dll": "USER32", "name": "MessageBoxA", "address": "0x00405004", "ordinal": 5}
          ],
          "functions": []
        }
        """;

    var result = invokeParseOutput(json);

    assertNotNull(result);
    assertEquals(2, result.imports().size());
    assertEquals("KERNEL32", result.imports().get(0).dll());
    assertEquals("CreateFileA", result.imports().get(0).name());
    assertEquals("0x00405000", result.imports().get(0).address());
    assertEquals(0, result.imports().get(0).ordinal());
    assertEquals("USER32", result.imports().get(1).dll());
    assertEquals("MessageBoxA", result.imports().get(1).name());
    assertEquals(5, result.imports().get(1).ordinal());
  }

  @Test
  void shouldReturnEmptyImportsWhenBlockAbsent() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "functions": []
        }
        """;

    var result = invokeParseOutput(json);

    assertTrue(result.imports().isEmpty());
  }

  @Test
  void shouldParseFunctionWithStackFrameAndComments() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "strings": [],
          "imports": [],
          "functions": [
            {
              "function": "sub_1000",
              "address": "0x00401000",
              "size": 64,
              "flags": "",
              "xrefs": "",
              "assembly": "0x00401000  push ebp",
              "labels": "",
              "stackFrame": "[{\\"offset\\":4,\\"size\\":4,\\"name\\":\\"arg_0\\"}]",
              "comments": "[{\\"address\\":\\"0x00401005\\",\\"text\\":\\"; CODE XREF\\",\\"repeatable\\":false}]"
            }
          ]
        }
        """;

    var result = invokeParseOutput(json);

    assertNotNull(result);
    assertEquals(1, result.functions().size());
    var func = result.functions().get(0);
    assertEquals("sub_1000", func.name());
    assertTrue(func.stackFrame().contains("offset"));
    assertTrue(func.stackFrame().contains("arg_0"));
    assertTrue(func.comments().contains("CODE XREF"));
    assertTrue(func.comments().contains("repeatable"));
  }

  @Test
  void shouldParseFunctionWithSignature() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "strings": [],
          "imports": [],
          "functions": [
            {
              "function": "sub_1000",
              "address": "0x00401000",
              "size": 64,
              "flags": "",
              "xrefs": "",
              "assembly": "0x00401000  push ebp",
              "labels": "",
              "stackFrame": "[]",
              "comments": "[]",
              "signature": "int __cdecl sub_1000(HWND hWnd, LPCSTR lpText)"
            }
          ]
        }
        """;

    var result = invokeParseOutput(json);

    assertEquals(
        "int __cdecl sub_1000(HWND hWnd, LPCSTR lpText)", result.functions().get(0).signature());
  }

  @Test
  void shouldDefaultSignatureToEmptyWhenFieldMissing() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "strings": [],
          "imports": [],
          "functions": [
            {
              "function": "sub_2000",
              "address": "0x00402000",
              "size": 32,
              "flags": "",
              "xrefs": "",
              "assembly": "0x00402000  ret",
              "labels": "",
              "stackFrame": "[]",
              "comments": "[]"
            }
          ]
        }
        """;

    var result = invokeParseOutput(json);

    assertEquals("", result.functions().get(0).signature());
  }

  @Test
  void shouldDefaultReferencedByToEmptyWhenFieldMissing() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "strings": [
            {"address": "0x00403000", "value": "legacy", "encoding": "ASCSTR_C"}
          ],
          "imports": [],
          "functions": []
        }
        """;

    var result = invokeParseOutput(json);

    assertNotNull(result);
    assertEquals(1, result.strings().size());
    assertEquals("legacy", result.strings().get(0).value());
    assertEquals("", result.strings().get(0).referencedBy());
  }

  @Test
  void shouldParseGlobalVars() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "strings": [],
          "imports": [],
          "globalVars": [
            {"name": "g_score", "address": "0x0040A000", "segment": ".data", "size": 4, "dataXrefs": "[{\\"function\\":\\"sub_1000\\",\\"address\\":\\"0x00401005\\",\\"type\\":\\"READ\\"}]"},
            {"name": "g_count", "address": "0x0040A004", "segment": ".data", "size": 2, "dataXrefs": "[]"}
          ],
          "functions": []
        }
        """;

    var result = invokeParseOutput(json);

    assertNotNull(result);
    assertEquals(2, result.globalVars().size());
    assertEquals("g_score", result.globalVars().get(0).name());
    assertEquals("0x0040A000", result.globalVars().get(0).address());
    assertEquals(".data", result.globalVars().get(0).segment());
    assertEquals(4, result.globalVars().get(0).size());
    assertTrue(result.globalVars().get(0).dataXrefs().contains("sub_1000"));
    assertEquals("g_count", result.globalVars().get(1).name());
    assertEquals("[]", result.globalVars().get(1).dataXrefs());
  }

  @Test
  void shouldReturnEmptyGlobalVarsWhenBlockAbsent() {
    var json =
        """
        {
          "entry_point": "0x00401000:start",
          "segments": [],
          "functions": []
        }
        """;

    var result = invokeParseOutput(json);

    assertTrue(result.globalVars().isEmpty());
  }

  private AnalysisResult invokeParseOutput(String json) {
    try {
      var method = AbstractIdaEngineService.class.getDeclaredMethod("parseOutput", String.class);
      method.setAccessible(true);

      var dummyService =
          new AbstractIdaEngineService(null, null) {
            @Override
            protected String getDockerfilePath() {
              return null;
            }

            @Override
            protected String getConfigKey() {
              return null;
            }

            @Override
            protected String getTempDirPrefix() {
              return null;
            }

            @Override
            public ai.decompile.engine.model.EngineType supportedEngine() {
              return ai.decompile.engine.model.EngineType.IDA66;
            }
          };

      return (AnalysisResult) method.invoke(dummyService, json);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}
