/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.fory.xlang;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.apache.fory.logging.Logger;
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.test.TestUtils;
import org.testng.Assert;
import org.testng.SkipException;
import org.testng.annotations.Test;

/** Executes cross-language tests against the Swift implementation. */
@Test
public class SwiftXlangTest extends XlangTestBase {
  private static final Logger LOG = LoggerFactory.getLogger(SwiftXlangTest.class);
  private static final String SWIFT_EXECUTABLE = "swift";
  private static final String SWIFT_PEER_TARGET = "ForyXlangTests";
  private static final File SWIFT_WORK_DIR = new File("../../swift");
  private static final Path SWIFT_WORK_DIR_PATH = SWIFT_WORK_DIR.toPath();
  private static final Path SWIFT_PEER_BINARY_PATH =
      SWIFT_WORK_DIR_PATH.resolve(".build/release").resolve(SWIFT_PEER_TARGET);
  private String swiftPeerBinaryPath;

  @Override
  protected void ensurePeerReady() {
    LOG.info("Starting ensurePeerReady for Swift xlang tests");
    String enabled = System.getenv("FORY_SWIFT_JAVA_CI");
    if (!"1".equals(enabled)) {
      throw new SkipException("Skipping SwiftXlangTest: FORY_SWIFT_JAVA_CI not set to 1");
    }
    boolean swiftInstalled = true;
    try {
      Process process = new ProcessBuilder(SWIFT_EXECUTABLE, "--version").start();
      int exitCode = process.waitFor();
      if (exitCode != 0) {
        swiftInstalled = false;
      }
    } catch (IOException | InterruptedException e) {
      swiftInstalled = false;
      if (e instanceof InterruptedException) {
        Thread.currentThread().interrupt();
      }
    }
    Assert.assertTrue(swiftInstalled, "swift is required for SwiftXlangTest");

    if (isPeerBinaryUpToDate()) {
      swiftPeerBinaryPath = SWIFT_PEER_BINARY_PATH.toAbsolutePath().toString();
      LOG.info("Completed ensurePeerReady for Swift xlang tests");
      return;
    }

    List<String> fastBuildCommand =
        Arrays.asList(
            SWIFT_EXECUTABLE,
            "build",
            "-c",
            "release",
            "--disable-automatic-resolution",
            "--product",
            SWIFT_PEER_TARGET);
    boolean built =
        TestUtils.executeCommand(
            fastBuildCommand,
            600,
            Collections.singletonMap("ENABLE_FORY_DEBUG_OUTPUT", "1"),
            SWIFT_WORK_DIR);
    if (!built) {
      List<String> fallbackBuildCommand =
          Arrays.asList(SWIFT_EXECUTABLE, "build", "-c", "release", "--product", SWIFT_PEER_TARGET);
      built =
          TestUtils.executeCommand(
              fallbackBuildCommand,
              600,
              Collections.singletonMap("ENABLE_FORY_DEBUG_OUTPUT", "1"),
              SWIFT_WORK_DIR);
    }
    Assert.assertTrue(built, "failed to build Swift xlang peer target " + SWIFT_PEER_TARGET);

    Path peerBinary = SWIFT_PEER_BINARY_PATH;
    Assert.assertTrue(
        Files.isRegularFile(peerBinary),
        "Swift xlang peer binary not found at " + peerBinary.toAbsolutePath());
    Assert.assertTrue(
        Files.isExecutable(peerBinary),
        "Swift xlang peer binary is not executable: " + peerBinary.toAbsolutePath());
    swiftPeerBinaryPath = peerBinary.toAbsolutePath().toString();
    LOG.info("Completed ensurePeerReady for Swift xlang tests");
  }

  @Override
  protected CommandContext buildCommandContext(String caseName, Path dataFile) {
    Assert.assertNotNull(swiftPeerBinaryPath, "Swift xlang peer binary path is not initialized");
    List<String> command = Arrays.asList(swiftPeerBinaryPath, "--case", caseName);

    Map<String, String> env = envBuilder(dataFile);
    env.put("ENABLE_FORY_DEBUG_OUTPUT", "1");
    return new CommandContext(command, env, SWIFT_WORK_DIR);
  }

  private boolean isPeerBinaryUpToDate() {
    if (!Files.isRegularFile(SWIFT_PEER_BINARY_PATH)
        || !Files.isExecutable(SWIFT_PEER_BINARY_PATH)) {
      return false;
    }
    try {
      FileTime binaryMTime = Files.getLastModifiedTime(SWIFT_PEER_BINARY_PATH);
      if (isFileNewerThan(binaryMTime, SWIFT_WORK_DIR_PATH.resolve("Package.swift"))
          || isFileNewerThan(binaryMTime, SWIFT_WORK_DIR_PATH.resolve("Package.resolved"))) {
        return false;
      }
      return !hasNewerFileInTree(binaryMTime, SWIFT_WORK_DIR_PATH.resolve("Sources"));
    } catch (IOException e) {
      return false;
    }
  }

  private static boolean hasNewerFileInTree(FileTime binaryMTime, Path root) throws IOException {
    if (!Files.isDirectory(root)) {
      return true;
    }
    try (Stream<Path> files = Files.walk(root)) {
      return files
          .filter(Files::isRegularFile)
          .anyMatch(path -> isFileNewerThanUnchecked(binaryMTime, path));
    }
  }

  private static boolean isFileNewerThan(FileTime binaryMTime, Path path) throws IOException {
    return Files.isRegularFile(path) && Files.getLastModifiedTime(path).compareTo(binaryMTime) > 0;
  }

  private static boolean isFileNewerThanUnchecked(FileTime binaryMTime, Path path) {
    try {
      return Files.getLastModifiedTime(path).compareTo(binaryMTime) > 0;
    } catch (IOException e) {
      return true;
    }
  }

  // ============================================================================
  // Test methods - duplicated from XlangTestBase for Maven Surefire discovery
  // ============================================================================

  @Test(groups = "xlang")
  public void testBuffer() throws java.io.IOException {
    super.testBuffer();
  }

  @Test(groups = "xlang")
  public void testBufferVar() throws java.io.IOException {
    super.testBufferVar();
  }

  @Test(groups = "xlang")
  public void testMurmurHash3() throws java.io.IOException {
    super.testMurmurHash3();
  }

  @Test(groups = "xlang")
  public void testStringSerializer() throws Exception {
    super.testStringSerializer();
  }

  @Test(groups = "xlang")
  public void testCrossLanguageSerializer() throws Exception {
    super.testCrossLanguageSerializer();
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testSimpleStruct(boolean enableCodegen) throws java.io.IOException {
    super.testSimpleStruct(enableCodegen);
  }

  @Test(groups = "xlang")
  public void testSimpleNamedStructCodegenEnabled() throws java.io.IOException {
    super.testSimpleNamedStruct(false);
  }

  @Test(groups = "xlang")
  public void testSimpleNamedStructCodegenDisabled() throws java.io.IOException {
    super.testSimpleNamedStruct(false);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testStructEvolvingOverride(boolean enableCodegen) throws java.io.IOException {
    super.testStructEvolvingOverride(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testList(boolean enableCodegen) throws java.io.IOException {
    super.testList(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testMap(boolean enableCodegen) throws java.io.IOException {
    super.testMap(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testInteger(boolean enableCodegen) throws java.io.IOException {
    super.testInteger(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testItem(boolean enableCodegen) throws java.io.IOException {
    super.testItem(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testColor(boolean enableCodegen) throws java.io.IOException {
    super.testColor(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testStructWithList(boolean enableCodegen) throws java.io.IOException {
    super.testStructWithList(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testStructWithMap(boolean enableCodegen) throws java.io.IOException {
    super.testStructWithMap(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testCollectionElementRefOverride(boolean enableCodegen) throws java.io.IOException {
    super.testCollectionElementRefOverride(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testSkipIdCustom(boolean enableCodegen) throws java.io.IOException {
    super.testSkipIdCustom(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testSkipNameCustom(boolean enableCodegen) throws java.io.IOException {
    super.testSkipNameCustom(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testConsistentNamed(boolean enableCodegen) throws java.io.IOException {
    super.testConsistentNamed(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testStructVersionCheck(boolean enableCodegen) throws java.io.IOException {
    super.testStructVersionCheck(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testPolymorphicList(boolean enableCodegen) throws java.io.IOException {
    super.testPolymorphicList(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testPolymorphicMap(boolean enableCodegen) throws java.io.IOException {
    super.testPolymorphicMap(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testOneStringFieldSchemaConsistent(boolean enableCodegen) throws java.io.IOException {
    super.testOneStringFieldSchemaConsistent(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testOneStringFieldCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testOneStringFieldCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testTwoStringFieldCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testTwoStringFieldCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testSchemaEvolutionCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testSchemaEvolutionCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testOneEnumFieldSchemaConsistent(boolean enableCodegen) throws java.io.IOException {
    super.testOneEnumFieldSchemaConsistent(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testOneEnumFieldCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testOneEnumFieldCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testTwoEnumFieldCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testTwoEnumFieldCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testEnumSchemaEvolutionCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testEnumSchemaEvolutionCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testNullableFieldSchemaConsistentNotNull(boolean enableCodegen)
      throws java.io.IOException {
    super.testNullableFieldSchemaConsistentNotNull(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testNullableFieldSchemaConsistentNull(boolean enableCodegen)
      throws java.io.IOException {
    super.testNullableFieldSchemaConsistentNull(enableCodegen);
  }

  @Override
  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testNullableFieldCompatibleNotNull(boolean enableCodegen) throws java.io.IOException {
    super.testNullableFieldCompatibleNotNull(enableCodegen);
  }

  @Override
  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testNullableFieldCompatibleNull(boolean enableCodegen) throws java.io.IOException {
    super.testNullableFieldCompatibleNull(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testUnionXlang(boolean enableCodegen) throws java.io.IOException {
    super.testUnionXlang(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testRefSchemaConsistent(boolean enableCodegen) throws java.io.IOException {
    super.testRefSchemaConsistent(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testRefCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testRefCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testCircularRefSchemaConsistent(boolean enableCodegen) throws java.io.IOException {
    super.testCircularRefSchemaConsistent(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testCircularRefCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testCircularRefCompatible(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testUnsignedSchemaConsistent(boolean enableCodegen) throws java.io.IOException {
    super.testUnsignedSchemaConsistent(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testUnsignedSchemaConsistentSimple(boolean enableCodegen) throws java.io.IOException {
    super.testUnsignedSchemaConsistentSimple(enableCodegen);
  }

  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
  public void testUnsignedSchemaCompatible(boolean enableCodegen) throws java.io.IOException {
    super.testUnsignedSchemaCompatible(enableCodegen);
  }
}
