/*
 * 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.
 */

#include <gtest/gtest.h>
#include <tvm/ir/expr.h>
#include <tvm/runtime/logging.h>
#include <tvm/target/target.h>

#include <cmath>
#include <string>

using namespace tvm;

TVM_REGISTER_TARGET_KIND("TestTargetKind", kDLCPU)
    .set_attr<std::string>("Attr1", "Value1")
    .add_attr_option<bool>("my_bool")
    .add_attr_option<ffi::Array<ffi::String>>("your_names")
    .add_attr_option<ffi::Map<ffi::String, int64_t>>("her_maps");

ffi::Map<ffi::String, ffi::Any> TestTargetParser(ffi::Map<ffi::String, ffi::Any> target) {
  ffi::String mcpu = target.at("mcpu").as_or_throw<ffi::String>();
  target.Set("mcpu", ffi::String("super_") + mcpu);
  target.Set("keys", ffi::Array<ffi::String>({"super"}));
  target.Set("feature.test", true);
  return target;
}

ffi::Map<ffi::String, ffi::Any> TestAttrsPreProcessor(ffi::Map<ffi::String, ffi::Any> attrs) {
  attrs.Set("mattr", ffi::String("woof"));
  return attrs;
}

TVM_REGISTER_TARGET_KIND("TestTargetParser", kDLCPU)
    .add_attr_option<ffi::String>("mattr")
    .add_attr_option<ffi::String>("mcpu")
    .set_default_keys({"cpu"})
    .set_target_canonicalizer(TestTargetParser);

TVM_REGISTER_TARGET_KIND("TestAttrsPreprocessor", kDLCPU)
    .add_attr_option<ffi::String>("mattr")
    .set_default_keys({"cpu"})
    .set_target_canonicalizer(TestAttrsPreProcessor);

TVM_REGISTER_TARGET_KIND("TestClashingPreprocessor", kDLCPU)
    .add_attr_option<ffi::String>("mattr")
    .add_attr_option<ffi::String>("mcpu")
    .set_default_keys({"cpu"})
    .set_target_canonicalizer(TestTargetParser);

TEST(TargetKind, GetAttrMap) {
  auto map = tvm::TargetKind::GetAttrMap<std::string>("Attr1");
  auto target_kind = tvm::TargetKind::Get("TestTargetKind").value();
  std::string result = map[target_kind];
  TVM_FFI_ICHECK_EQ(result, "Value1");
}

TEST(TargetCreation, NestedConfig) {
  ffi::Map<ffi::String, ffi::Any> config = {
      {"my_bool", true},
      {"your_names", ffi::Array<ffi::String>{"junru", "jian"}},
      {"kind", ffi::String("TestTargetKind")},
      {
          "her_maps",
          ffi::Map<ffi::String, int64_t>{
              {"a", 1},
              {"b", 2},
          },
      },
  };
  Target target = Target(config);
  TVM_FFI_ICHECK_EQ(target->kind, TargetKind::Get("TestTargetKind").value());
  TVM_FFI_ICHECK_EQ(target->tag, "");
  TVM_FFI_ICHECK(target->keys.empty());
  bool my_bool = target->GetAttr<bool>("my_bool").value();
  TVM_FFI_ICHECK_EQ(my_bool, true);
  ffi::Array<ffi::String> your_names =
      target->GetAttr<ffi::Array<ffi::String>>("your_names").value();
  TVM_FFI_ICHECK_EQ(your_names.size(), 2U);
  TVM_FFI_ICHECK_EQ(your_names[0], "junru");
  TVM_FFI_ICHECK_EQ(your_names[1], "jian");
  ffi::Map<ffi::String, int64_t> her_maps =
      target->GetAttr<ffi::Map<ffi::String, int64_t>>("her_maps").value();
  TVM_FFI_ICHECK_EQ(her_maps.size(), 2U);
  TVM_FFI_ICHECK_EQ(her_maps["a"], 1);
  TVM_FFI_ICHECK_EQ(her_maps["b"], 2);
}

TEST(TargetCreationFail, UnrecognizedConfigOption) {
  ffi::Map<ffi::String, ffi::Any> config = {
      {"my_bool", true},
      {"your_names", ffi::Array<ffi::String>{"junru", "jian"}},
      {"kind", ffi::String("TestTargetKind")},
      {"bad", ffi::ObjectRef(nullptr)},
      {
          "her_maps",
          ffi::Map<ffi::String, int64_t>{
              {"a", 1},
              {"b", 2},
          },
      },
  };
  bool failed = false;
  try {
    Target tgt(config);
  } catch (...) {
    failed = true;
  }
  ASSERT_EQ(failed, true);
}

TEST(TargetCreationFail, TypeMismatch) {
  ffi::Map<ffi::String, ffi::Any> config = {
      {"my_bool", ffi::String("true")},
      {"your_names", ffi::Array<ffi::String>{"junru", "jian"}},
      {"kind", ffi::String("TestTargetKind")},
      {
          "her_maps",
          ffi::Map<ffi::String, int64_t>{
              {"a", 1},
              {"b", 2},
          },
      },
  };
  bool failed = false;
  try {
    Target tgt(config);
  } catch (...) {
    failed = true;
  }
  ASSERT_EQ(failed, true);
}

TEST(TargetCreationFail, TargetKindNotFound) {
  ffi::Map<ffi::String, ffi::Any> config = {
      {"my_bool", "true"},
      {"your_names", ffi::Array<ffi::String>{"junru", "jian"}},
      {
          "her_maps",
          ffi::Map<ffi::String, int64_t>{
              {"a", 1},
              {"b", 2},
          },
      },
  };
  bool failed = false;
  try {
    Target tgt(config);
  } catch (...) {
    failed = true;
  }
  ASSERT_EQ(failed, true);
}

TEST(TargetCreation, TargetParser) {
  Target test_target(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestTargetParser")},
      {"mcpu", ffi::String("woof")},
  });
  ASSERT_EQ(test_target->GetAttr<ffi::String>("mcpu").value(), "super_woof");
  ASSERT_EQ(test_target->keys.size(), 1);
  ASSERT_EQ(test_target->keys[0], "super");
}

TEST(TargetCreation, TargetFeatures) {
  Target test_target_with_parser(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestTargetParser")},
      {"mcpu", ffi::String("woof")},
  });
  // Features are stored as "feature.xxx" keys in attrs
  ASSERT_EQ(test_target_with_parser->GetAttr<bool>("feature.test").value(), true);

  Target test_target_no_parser("TestTargetKind");
  ASSERT_EQ(test_target_no_parser->GetAttr<bool>("feature.test"), std::nullopt);
  ASSERT_EQ(test_target_no_parser->GetAttr<bool>("feature.test", true).value(), true);
}

TEST(TargetCreation, TargetFeaturesSetByCanonicalizer) {
  // feature.* keys are set by the canonicalizer, not by user input.
  Target test_target(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestTargetParser")},
      {"mcpu", ffi::String("woof")},
  });
  // TestTargetParser sets "feature.test" = true
  ASSERT_EQ(test_target->GetAttr<bool>("feature.test").value(), true);
  // Non-existent features return nullopt
  ASSERT_EQ(test_target->GetAttr<bool>("feature.nonexistent"), std::nullopt);
}

TEST(TargetCreation, TargetAttrsPreProcessor) {
  Target test_target(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestAttrsPreprocessor")},
      {"mattr", ffi::String("cake")},
  });
  ASSERT_EQ(test_target->GetAttr<ffi::String>("mattr").value(), "woof");
}

TEST(TargetCreation, TargetParserProcessing) {
  Target test_target(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestClashingPreprocessor")},
      {"mcpu", ffi::String("woof")},
      {"mattr", ffi::String("cake")},
  });
  ASSERT_EQ(test_target->GetAttr<ffi::String>("mcpu").value(), "super_woof");
  ASSERT_EQ(test_target->GetAttr<ffi::String>("mattr").value(), "cake");
}

TEST(TargetCreation, RoundTripCanonicalizerFeatures) {
  // Construct a target whose canonicalizer sets feature.test and transforms mcpu
  Target original(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestTargetParser")},
      {"mcpu", ffi::String("woof")},
  });
  ASSERT_EQ(original->GetAttr<ffi::String>("mcpu").value(), "super_woof");
  ASSERT_EQ(original->GetAttr<bool>("feature.test").value(), true);

  // Export to config and reconstruct
  ffi::Map<ffi::String, ffi::Any> exported = original->ToConfig();
  Target reconstructed(exported);

  // Canonicalized attrs must survive the round-trip
  // Note: mcpu gets canonicalized again (super_super_woof) because the canonicalizer runs
  ASSERT_TRUE(reconstructed->GetAttr<ffi::String>("mcpu").has_value());
  ASSERT_EQ(reconstructed->GetAttr<bool>("feature.test").value(), true);
  ASSERT_EQ(reconstructed->keys.size(), 1);
  ASSERT_EQ(reconstructed->keys[0], "super");
}

TEST(TargetCreation, RoundTripCanonicalizerFeaturesNestedHost) {
  // Construct a host target whose canonicalizer sets feature.test
  Target host(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestTargetParser")},
      {"mcpu", ffi::String("woof")},
  });
  ASSERT_EQ(host->GetAttr<bool>("feature.test").value(), true);

  // Attach it as host to another target
  Target outer(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestTargetKind")},
      {"my_bool", true},
  });
  Target combined(outer, host);

  // Export the outer target (includes nested host) and reconstruct
  ffi::Map<ffi::String, ffi::Any> exported = combined->ToConfig();
  Target reconstructed(exported);

  // The nested host must reconstruct successfully with feature.* preserved
  ffi::Optional<Target> reconstructed_host = reconstructed->GetHost();
  ASSERT_TRUE(reconstructed_host.has_value());
  ASSERT_EQ(reconstructed_host.value()->GetAttr<bool>("feature.test").value(), true);
  ASSERT_TRUE(reconstructed_host.value()->GetAttr<ffi::String>("mcpu").has_value());
}

TEST(TargetCreationFail, UnknownNonFeatureKeyStillFails) {
  // Verify that unknown non-feature.* keys still fail schema validation
  ffi::Map<ffi::String, ffi::Any> config = {
      {"kind", ffi::String("TestTargetParser")},
      {"mcpu", ffi::String("woof")},
      {"unknown_key", ffi::String("bad")},
  };
  ASSERT_THROW({ Target{config}; }, tvm::ffi::Error);
}

TVM_REGISTER_TARGET_KIND("TestStringKind", kDLCPU)
    .add_attr_option<ffi::String>("single")
    .add_attr_option<ffi::Array<ffi::String>>("array")
    .add_attr_option<ffi::Array<ffi::Array<ffi::String>>>("nested-array")
    .add_attr_option<ffi::Array<ffi::Array<ffi::Array<ffi::String>>>>("nested2-array");

TEST(TargetCreation, ProcessStrings) {
  // Test single string attribute
  Target test_target1(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestStringKind")},
      {"single", ffi::String("'string with single quote")},
  });
  ASSERT_TRUE(test_target1->GetAttr<ffi::String>("single"));
  ffi::String string1 = test_target1->GetAttr<ffi::String>("single").value();
  ASSERT_EQ(string1, "'string with single quote");

  // Test array of strings
  Target test_target3(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestStringKind")},
      {"array", ffi::Array<ffi::String>{"-danny", "-sammy=1", "-kirby=string with space"}},
  });
  ASSERT_TRUE(test_target3->GetAttr<ffi::Array<ffi::String>>("array"));
  ffi::Array<ffi::String> array3 = test_target3->GetAttr<ffi::Array<ffi::String>>("array").value();
  ASSERT_EQ(array3[0], "-danny");
  ASSERT_EQ(array3[1], "-sammy=1");
  ASSERT_EQ(array3[2], "-kirby=string with space");

  // Test nested array of strings
  Target test_target6(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestStringKind")},
      {"nested-array",
       ffi::Array<ffi::Array<ffi::String>>{
           ffi::Array<ffi::String>{"foo0", "foo1", "foo2"},
           ffi::Array<ffi::String>{"bar0", "bar1", "bar2"},
           ffi::Array<ffi::String>{"baz0", "baz1"},
       }},
  });
  ASSERT_TRUE(test_target6->GetAttr<ffi::Array<ffi::Array<ffi::String>>>("nested-array"));
  ffi::Array<ffi::Array<ffi::String>> array6 =
      test_target6->GetAttr<ffi::Array<ffi::Array<ffi::String>>>("nested-array").value();
  ASSERT_EQ(array6[0][0], "foo0");
  ASSERT_EQ(array6[0][1], "foo1");
  ASSERT_EQ(array6[0][2], "foo2");
  ASSERT_EQ(array6[1][0], "bar0");
  ASSERT_EQ(array6[1][1], "bar1");
  ASSERT_EQ(array6[1][2], "bar2");
  ASSERT_EQ(array6[2][0], "baz0");
  ASSERT_EQ(array6[2][1], "baz1");

  // Test doubly-nested array of strings
  Target test_target7(ffi::Map<ffi::String, ffi::Any>{
      {"kind", ffi::String("TestStringKind")},
      {"nested2-array",
       ffi::Array<ffi::Array<ffi::Array<ffi::String>>>{
           ffi::Array<ffi::Array<ffi::String>>{
               ffi::Array<ffi::String>{"foo0", "foo1"},
               ffi::Array<ffi::String>{"bar0", "bar1"},
               ffi::Array<ffi::String>{"baz0", "baz1"},
           },
           ffi::Array<ffi::Array<ffi::String>>{
               ffi::Array<ffi::String>{"zing0", "zing1"},
               ffi::Array<ffi::String>{"fred"},
           },
       }},
  });
  ASSERT_TRUE(
      test_target7->GetAttr<ffi::Array<ffi::Array<ffi::Array<ffi::String>>>>("nested2-array"));
  ffi::Array<ffi::Array<ffi::Array<ffi::String>>> array7 =
      test_target7->GetAttr<ffi::Array<ffi::Array<ffi::Array<ffi::String>>>>("nested2-array")
          .value();
  ASSERT_EQ(array7.size(), 2);
  ASSERT_EQ(array7[0].size(), 3);
  ASSERT_EQ(array7[0][0].size(), 2);
  ASSERT_EQ(array7[0][1].size(), 2);
  ASSERT_EQ(array7[0][2].size(), 2);
  ASSERT_EQ(array7[1].size(), 2);
  ASSERT_EQ(array7[1][0].size(), 2);
  ASSERT_EQ(array7[1][1].size(), 1);

  ASSERT_EQ(array7[0][0][0], "foo0");
  ASSERT_EQ(array7[0][0][1], "foo1");
  ASSERT_EQ(array7[0][1][0], "bar0");
  ASSERT_EQ(array7[0][1][1], "bar1");
  ASSERT_EQ(array7[0][2][0], "baz0");
  ASSERT_EQ(array7[0][2][1], "baz1");
  ASSERT_EQ(array7[1][0][0], "zing0");
  ASSERT_EQ(array7[1][0][1], "zing1");
  ASSERT_EQ(array7[1][1][0], "fred");
}

TEST(TargetCreation, DeduplicateKeys) {
  ffi::Map<ffi::String, ffi::Any> config = {
      {"kind", ffi::String("llvm")},
      {"keys", ffi::Array<ffi::String>{"cpu", "arm_cpu"}},
      {"device", ffi::String("arm_cpu")},
  };
  Target target = Target(config);
  TVM_FFI_ICHECK_EQ(target->kind, TargetKind::Get("llvm").value());
  TVM_FFI_ICHECK_EQ(target->tag, "");
  TVM_FFI_ICHECK_EQ(target->keys.size(), 2U);
  TVM_FFI_ICHECK_EQ(target->keys[0], "cpu");
  TVM_FFI_ICHECK_EQ(target->keys[1], "arm_cpu");
  TVM_FFI_ICHECK_EQ(target->attrs.count("keys"), 0U);
  TVM_FFI_ICHECK_EQ(target->GetAttr<ffi::String>("device"), "arm_cpu");
}

TEST(TargetKindRegistry, ListTargetKinds) {
  ffi::Array<ffi::String> names = TargetKindRegEntry::ListTargetKinds();
  TVM_FFI_ICHECK_EQ(names.empty(), false);
  TVM_FFI_ICHECK_EQ(std::count(std::begin(names), std::end(names), "llvm"), 1);
}

TEST(TargetKindRegistry, ListTargetOptions) {
  TargetKind llvm = TargetKind::Get("llvm").value();
  ffi::Map<ffi::String, ffi::String> attrs = TargetKindRegEntry::ListTargetKindOptions(llvm);
  TVM_FFI_ICHECK_EQ(attrs.empty(), false);
}
