// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/ui/cocoa/applescript/apple_event_util.h"

#include <CoreServices/CoreServices.h>
#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <string>

#include "base/json/json_reader.h"
#include "base/mac/scoped_aedesc.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#import "chrome/browser/ui/cocoa/test/cocoa_test_helper.h"
#include "testing/gtest_mac.h"

namespace {

std::string FourCharToString(FourCharCode code) {
  std::string result(6, '\'');
  result[1] = (code >> 24) & 0xFF;
  result[2] = (code >> 16) & 0xFF;
  result[3] = (code >> 8) & 0xFF;
  result[4] = code & 0xFF;
  return result;
}

// This function returns a string description of the contents of the given
// AEDesc in the AEGizmos/AEPrintDescToHandle format.
//
// The -[NSAppleEventDescriptor description] method does this too, but the
// problem is that it is implemented using AEPrintDescToHandle, which is both
// flaky <http://crbug.com/40317263> and constantly buffer-overflows and fails
// ASan tests <http://crbug.com/40302460>.
//
// This function does not handle every type that AEPrintDescToHandle does, but
// it covers the cases hit by the unit test, and fails in an obvious way should
// the Apple Event code change.
std::string AEDescToString(const AEDesc* aedesc) {
  switch (aedesc->descriptorType) {
    case typeType: {
      FourCharCode code;
      OSErr err = AEGetDescData(aedesc, &code, sizeof(code));
      if (err != noErr) {
        NOTREACHED();
      }

      return FourCharToString(code);
    }
    case typeSInt16:
    case typeSInt32:
    case typeSInt64: {
      base::mac::ScopedAEDesc<> wide_desc;
      OSErr err = AECoerceDesc(aedesc, typeSInt64, wide_desc.OutPointer());
      if (err != noErr) {
        NOTREACHED();
      }

      int64_t value;
      err = AEGetDescData(wide_desc, &value, sizeof(value));
      if (err != noErr) {
        NOTREACHED();
      }

      return base::NumberToString(value);
    }
    case typeIEEE32BitFloatingPoint:
    case typeIEEE64BitFloatingPoint: {
      base::mac::ScopedAEDesc<> wide_desc;
      OSErr err = AECoerceDesc(aedesc, typeIEEE64BitFloatingPoint,
                               wide_desc.OutPointer());
      if (err != noErr) {
        NOTREACHED();
      }

      double value;
      err = AEGetDescData(wide_desc, &value, sizeof(value));
      if (err != noErr) {
        NOTREACHED();
      }

      return base::NumberToString(value);
    }
    // Text formats look like:
    //  'utxt'("string here")
    case typeUnicodeText: {
      size_t byte_length = AEGetDescDataSize(aedesc);
      std::vector<char16_t> data_vector(byte_length / sizeof(char16_t));
      OSErr err = AEGetDescData(aedesc, data_vector.data(), byte_length);
      if (err != noErr) {
        NOTREACHED();
      }
      return FourCharToString(typeUnicodeText) + "(\"" +
             base::UTF16ToUTF8(
                 std::u16string(data_vector.begin(), data_vector.end())) +
             "\")";
    }
    // Lists look like:
    //  [ item1, item2, item3 ]
    // and records look like:
    //  { 'key1':value1, 'key2': value2 }
    case typeAEList:
    case typeAERecord: {
      bool is_record = aedesc->descriptorType == typeAERecord;

      std::string result = is_record ? "{ " : "[ ";
      long list_count;
      OSErr err = AECountItems(aedesc, &list_count);
      if (err != noErr) {
        NOTREACHED();
      }
      for (long i = 0; i < list_count; ++i) {
        AEKeyword key;
        base::mac::ScopedAEDesc<> value_desc;
        err = AEGetNthDesc(aedesc, i + 1 /* 1-based! */, typeWildCard, &key,
                           value_desc.OutPointer());
        if (err != noErr) {
          NOTREACHED();
        }

        if (is_record) {
          result += FourCharToString(key);
          result += ":";
        }

        result += AEDescToString(value_desc);

        if (i < list_count - 1) {
          result += ", ";
        }
      }

      result += is_record ? " }" : " ]";
      return result;
    }
    default: {
      NOTREACHED() << "unexpected descriptor type "
                   << FourCharToString(aedesc->descriptorType);
    }
  }
}

class AppleEventUtilTest : public CocoaTest {};

struct TestCase {
  const char* json_input;
  const char* expected_aedesc_dump;
  DescType expected_aedesc_type;
};

TEST_F(AppleEventUtilTest, ValueToAppleEventDescriptor) {
  const struct TestCase cases[] = {
      {"null", "'msng'", typeType},
      {"-1000", "-1000", typeSInt32},
      {"0", "0", typeSInt32},
      {"1000", "1000", typeSInt32},
      {"-1e100", "-1e+100", typeIEEE64BitFloatingPoint},
      {"0.0", "0", typeIEEE64BitFloatingPoint},
      {"1e100", "1e+100", typeIEEE64BitFloatingPoint},
      {"\"\"", "'utxt'(\"\")", typeUnicodeText},
      {"\"string\"", "'utxt'(\"string\")", typeUnicodeText},
      {"{}", "{ 'usrf':[  ] }", typeAERecord},
      {"[]", "[  ]", typeAEList},
      {"{\"Image\": {"
       "\"Width\": 800,"
       "\"Height\": 600,"
       "\"Title\": \"View from 15th Floor\","
       "\"Thumbnail\": {"
       "\"Url\": \"http://www.example.com/image/481989943\","
       "\"Height\": 125,"
       "\"Width\": \"100\""
       "},"
       "\"IDs\": [116, 943, 234, 38793]"
       "}"
       "}",
       "{ 'usrf':[ 'utxt'(\"Image\"), { 'usrf':[ 'utxt'(\"Height\"), 600, "
       "'utxt'(\"IDs\"), [ 116, 943, 234, 38793 ], 'utxt'(\"Thumbnail\"), "
       "{ 'usrf':[ 'utxt'(\"Height\"), 125, 'utxt'(\"Url\"), "
       "'utxt'(\"http://www.example.com/image/481989943\"), 'utxt'(\"Width\"), "
       "'utxt'(\"100\") ] }, 'utxt'(\"Title\"), "
       "'utxt'(\"View from 15th Floor\"), 'utxt'(\"Width\"), 800 ] } ] }",
       typeAERecord},
      {"["
       "{"
       "\"precision\": \"zip\","
       "\"Latitude\": 37.7668,"
       "\"Longitude\": -122.3959,"
       "\"Address\": \"\","
       "\"City\": \"SAN FRANCISCO\","
       "\"State\": \"CA\","
       "\"Zip\": \"94107\","
       "\"Country\": \"US\""
       "},"
       "{"
       "\"precision\": \"zip\","
       "\"Latitude\": 37.371991,"
       "\"Longitude\": -122.026020,"
       "\"Address\": \"\","
       "\"City\": \"SUNNYVALE\","
       "\"State\": \"CA\","
       "\"Zip\": \"94085\","
       "\"Country\": \"US\""
       "}"
       "]",
       "[ { 'usrf':[ 'utxt'(\"Address\"), 'utxt'(\"\"), 'utxt'(\"City\"), "
       "'utxt'(\"SAN FRANCISCO\"), 'utxt'(\"Country\"), 'utxt'(\"US\"), "
       "'utxt'(\"Latitude\"), 37.7668, 'utxt'(\"Longitude\"), -122.3959, "
       "'utxt'(\"State\"), 'utxt'(\"CA\"), 'utxt'(\"Zip\"), 'utxt'(\"94107\"), "
       "'utxt'(\"precision\"), 'utxt'(\"zip\") ] }, { 'usrf':[ "
       "'utxt'(\"Address\"), 'utxt'(\"\"), 'utxt'(\"City\"), "
       "'utxt'(\"SUNNYVALE\"), 'utxt'(\"Country\"), 'utxt'(\"US\"), "
       "'utxt'(\"Latitude\"), 37.371991, 'utxt'(\"Longitude\"), -122.02602, "
       "'utxt'(\"State\"), 'utxt'(\"CA\"), 'utxt'(\"Zip\"), 'utxt'(\"94085\"), "
       "'utxt'(\"precision\"), 'utxt'(\"zip\") ] } ]",
       typeAEList},
  };

  int i = 0;
  for (const auto& test_case : cases) {
    std::optional<base::Value> value = base::JSONReader::Read(
        test_case.json_input, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
    NSAppleEventDescriptor* descriptor =
        chrome::mac::ValueToAppleEventDescriptor(value.value());

    EXPECT_EQ(test_case.expected_aedesc_dump, AEDescToString(descriptor.aeDesc))
        << "i: " << i;
    EXPECT_EQ(test_case.expected_aedesc_type, descriptor.descriptorType)
        << "i: " << i;
    ++i;
  }

  // Test boolean values separately because boolean NSAppleEventDescriptors
  // return different values across different system versions when their
  // -description method is called.

  for (bool b : {true, false}) {
    base::Value value(b);
    NSAppleEventDescriptor* descriptor =
        chrome::mac::ValueToAppleEventDescriptor(value);

    EXPECT_EQ(typeBoolean, descriptor.descriptorType);
    EXPECT_EQ(b, descriptor.booleanValue);
  }
}

}  // namespace
