/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed 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 <unistd.h>
#include <string>

#include <android-base/file.h>
#include <android-base/test_utils.h>
#include <gtest/gtest.h>

#include "JavaGen.h"

namespace {

constexpr const char* kTestSyspropFile =
    R"(owner: Vendor
module: "com.somecompany.TestProperties"

prop {
    api_name: "test_double"
    type: Double
    prop_name: "vendor.test_double"
    scope: Internal
    access: ReadWrite
}
prop {
    api_name: "test_int"
    type: Integer
    prop_name: "vendor.test_int"
    scope: Public
    access: ReadWrite
}
prop {
    api_name: "test.string"
    type: String
    prop_name: "vendor.test.string"
    scope: System
    access: ReadWrite
}

prop {
    api_name: "test.enum"
    type: Enum
    prop_name: "vendor.test.enum"
    enum_values: "a|b|c|D|e|f|G"
    scope: Internal
    access: ReadWrite
}
prop {
    api_name: "test_BOOLeaN"
    type: Boolean
    prop_name: "ro.vendor.test.b"
    scope: Public
    access: Writeonce
}
prop {
    api_name: "vendor.os_test-long"
    type: Long
    scope: System
    access: ReadWrite
}

prop {
    api_name: "test_double_list"
    type: DoubleList
    scope: Internal
    access: ReadWrite
}
prop {
    api_name: "test_list_int"
    type: IntegerList
    scope: Public
    access: ReadWrite
}
prop {
    api_name: "test.strlist"
    type: StringList
    scope: System
    access: ReadWrite
}

prop {
    api_name: "el"
    type: EnumList
    enum_values: "enu|mva|lue"
    scope: Internal
    access: ReadWrite
}
)";

constexpr const char* kExpectedJavaOutput =
    R"(// Generated by the sysprop generator. DO NOT EDIT!

package com.somecompany;

import android.annotation.SystemApi;

import android.os.SystemProperties;
import java.util.ArrayList;
import java.util.function.Function;
import java.util.List;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;

public final class TestProperties {
    private TestProperties () {}

    private static Boolean tryParseBoolean(String str) {
        switch (str.toLowerCase()) {
            case "1":
            case "true":
                return Boolean.TRUE;
            case "0":
            case "false":
                return Boolean.FALSE;
            default:
                return null;
        }
    }

    private static Integer tryParseInteger(String str) {
        try {
            return Integer.valueOf(str);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private static Long tryParseLong(String str) {
        try {
            return Long.valueOf(str);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private static Double tryParseDouble(String str) {
        try {
            return Double.valueOf(str);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private static String tryParseString(String str) {
        return "".equals(str) ? null : str;
    }

    private static <T extends Enum<T>> T tryParseEnum(Class<T> enumType, String str) {
        try {
            return Enum.valueOf(enumType, str.toUpperCase());
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    private static <T> List<T> tryParseList(Function<String, T> elementParser, String str) {
        if ("".equals(str)) return new ArrayList<>();

        List<T> ret = new ArrayList<>();

        for (String element : str.split(",")) {
            ret.add(elementParser.apply(element));
        }

        return ret;
    }

    private static <T extends Enum<T>> List<T> tryParseEnumList(Class<T> enumType, String str) {
        if ("".equals(str)) return new ArrayList<>();

        List<T> ret = new ArrayList<>();

        for (String element : str.split(",")) {
            ret.add(tryParseEnum(enumType, element));
        }

        return ret;
    }

    private static <T> String formatList(List<T> list) {
        StringJoiner joiner = new StringJoiner(",");

        for (T element : list) {
            joiner.add(element == null ? "" : element.toString());
        }

        return joiner.toString();
    }

    private static <T extends Enum<T>> String formatEnumList(List<T> list, Function<T, String> elementFormatter) {
        StringJoiner joiner = new StringJoiner(",");

        for (T element : list) {
            joiner.add(element == null ? "" : elementFormatter.apply(element));
        }

        return joiner.toString();
    }

    /** @hide */
    public static Optional<Double> test_double() {
        String value = SystemProperties.get("vendor.test_double");
        return Optional.ofNullable(tryParseDouble(value));
    }

    /** @hide */
    public static void test_double(Double value) {
        SystemProperties.set("vendor.test_double", value == null ? "" : value.toString());
    }

    public static Optional<Integer> test_int() {
        String value = SystemProperties.get("vendor.test_int");
        return Optional.ofNullable(tryParseInteger(value));
    }

    /** @hide */
    public static void test_int(Integer value) {
        SystemProperties.set("vendor.test_int", value == null ? "" : value.toString());
    }

    /** @hide */
    @SystemApi
    public static Optional<String> test_string() {
        String value = SystemProperties.get("vendor.test.string");
        return Optional.ofNullable(tryParseString(value));
    }

    /** @hide */
    public static void test_string(String value) {
        SystemProperties.set("vendor.test.string", value == null ? "" : value.toString());
    }

    /** @hide */
    public static enum test_enum_values {
        A("a"),
        B("b"),
        C("c"),
        D("D"),
        E("e"),
        F("f"),
        G("G");
        private final String propValue;
        private test_enum_values(String propValue) {
            this.propValue = propValue;
        }
        public String getPropValue() {
            return propValue;
        }
    }

    /** @hide */
    public static Optional<test_enum_values> test_enum() {
        String value = SystemProperties.get("vendor.test.enum");
        return Optional.ofNullable(tryParseEnum(test_enum_values.class, value));
    }

    /** @hide */
    public static void test_enum(test_enum_values value) {
        SystemProperties.set("vendor.test.enum", value == null ? "" : value.getPropValue());
    }

    public static Optional<Boolean> test_BOOLeaN() {
        String value = SystemProperties.get("ro.vendor.test.b");
        return Optional.ofNullable(tryParseBoolean(value));
    }

    /** @hide */
    public static void test_BOOLeaN(Boolean value) {
        SystemProperties.set("ro.vendor.test.b", value == null ? "" : value.toString());
    }

    /** @hide */
    @SystemApi
    public static Optional<Long> vendor_os_test_long() {
        String value = SystemProperties.get("vendor.vendor.os_test-long");
        return Optional.ofNullable(tryParseLong(value));
    }

    /** @hide */
    public static void vendor_os_test_long(Long value) {
        SystemProperties.set("vendor.vendor.os_test-long", value == null ? "" : value.toString());
    }

    /** @hide */
    public static List<Double> test_double_list() {
        String value = SystemProperties.get("vendor.test_double_list");
        return tryParseList(v -> tryParseDouble(v), value);
    }

    /** @hide */
    public static void test_double_list(List<Double> value) {
        SystemProperties.set("vendor.test_double_list", value == null ? "" : formatList(value));
    }

    public static List<Integer> test_list_int() {
        String value = SystemProperties.get("vendor.test_list_int");
        return tryParseList(v -> tryParseInteger(v), value);
    }

    /** @hide */
    public static void test_list_int(List<Integer> value) {
        SystemProperties.set("vendor.test_list_int", value == null ? "" : formatList(value));
    }

    /** @hide */
    @SystemApi
    public static List<String> test_strlist() {
        String value = SystemProperties.get("vendor.test.strlist");
        return tryParseList(v -> tryParseString(v), value);
    }

    /** @hide */
    public static void test_strlist(List<String> value) {
        SystemProperties.set("vendor.test.strlist", value == null ? "" : formatList(value));
    }

    /** @hide */
    public static enum el_values {
        ENU("enu"),
        MVA("mva"),
        LUE("lue");
        private final String propValue;
        private el_values(String propValue) {
            this.propValue = propValue;
        }
        public String getPropValue() {
            return propValue;
        }
    }

    /** @hide */
    public static List<el_values> el() {
        String value = SystemProperties.get("vendor.el");
        return tryParseEnumList(el_values.class, value);
    }

    /** @hide */
    public static void el(List<el_values> value) {
        SystemProperties.set("vendor.el", value == null ? "" : formatEnumList(value, el_values::getPropValue));
    }
}
)";

}  // namespace

using namespace std::string_literals;

TEST(SyspropTest, JavaGenTest) {
  TemporaryFile temp_file;

  // strlen is optimized for constants, so don't worry about it.
  ASSERT_EQ(write(temp_file.fd, kTestSyspropFile, strlen(kTestSyspropFile)),
            strlen(kTestSyspropFile));
  close(temp_file.fd);
  temp_file.fd = -1;

  TemporaryDir temp_dir;

  std::string err;
  ASSERT_TRUE(GenerateJavaLibrary(temp_file.path, temp_dir.path, &err));
  ASSERT_TRUE(err.empty());

  std::string java_output_path =
      temp_dir.path + "/com/somecompany/TestProperties.java"s;

  std::string java_output;
  ASSERT_TRUE(
      android::base::ReadFileToString(java_output_path, &java_output, true));
  EXPECT_EQ(java_output, kExpectedJavaOutput);

  unlink(java_output_path.c_str());
  rmdir((temp_dir.path + "/com/somecompany"s).c_str());
  rmdir((temp_dir.path + "/com"s).c_str());
}