# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import sys
import unittest
from os import path

import mozpack.path as mozpath
import mozunit
import yaml

try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

sys.path.append(path.join(path.dirname(__file__), ".."))
from init.generate_static_pref_list import check_pref_list, generate_code

test_data_path = mozpath.abspath(mozpath.dirname(__file__))
test_data_path = mozpath.join(test_data_path, "data")

# A single good input with lots of different combinations.
good_input = """
- name: my.bool
  type: bool
  value: false
  mirror: never

- name: my.int
  type: int32_t
  value: -123
  mirror: once
  do_not_use_directly: false
  rust: false

- mirror: always
  value: 999
  type: uint32_t
  name: my.uint
  rust: true

- name: my.float            # A comment.
  type: float               # A comment.
  do_not_use_directly: true # A comment.
  value: 0.0f               # A comment.
  mirror: once              # A comment.
  rust: true                # A comment.

# A comment.
- name: my.string
  type: String
  value: foo"bar    # The double quote needs escaping.
  mirror: never
  include: foobar.h

# A comment.
- name: my.string2
  type: String
  value: "foobar"    # This string is quoted.
  mirror: never

# A comment.
- name: my.atomic.bool
  type: RelaxedAtomicBool
  value: true
  mirror: always
  rust: true

# A comment.
- name: my.datamutex.string
  type: DataMutexString
  value: "foobar"    # This string is quoted.
  mirror: always

# Mirrored string-valued prefs are interesting in Rust.
- name: my.datamutex.string.rust
  type: DataMutexString
  value: ""
  mirror: always
  rust: true

# YAML+Python interprets `10 + 10 * 20` as a string, and so it is printed
# unchanged.
- name: my.atomic.int
  type: ReleaseAcquireAtomicInt32
  value: 10 + 10 * 20
  mirror: always
  do_not_use_directly: true # A comment.

# YAML+Python changes `0x44` to `68` because it interprets the value as an
# integer.
- name: my.atomic.uint
  type: SequentiallyConsistentAtomicUint32
  value: 0x44
  mirror: once

# YAML+Python changes `.4455667` to `0.4455667` because it interprets the value
# as a float.
- name: my-dashed.atomic.float
  type: AtomicFloat
  value: .4455667
  mirror: never
  include: <math.h>
"""

# The corresponding code for good_input.
good = {}

good["static_pref_list_all_h"] = """\
// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT.

#include "mozilla/StaticPrefList_my.h"
#include "mozilla/StaticPrefList_my_dashed.h"
"""

good["static_prefs_all_h"] = """\
// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT.

#include "mozilla/StaticPrefs_my.h"
#include "mozilla/StaticPrefs_my_dashed.h"
"""

good["static_pref_list_group_h"] = {
    "my": """\
// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT.

NEVER_PREF("my.bool", bool, false)

ONCE_PREF(
  "my.int",
   my_int,
   my_int_AtStartup,
  int32_t, -123
)

ALWAYS_PREF(
  "my.uint",
   my_uint,
   my_uint,
  uint32_t, 999
)

ONCE_PREF(
  "my.float",
   my_float,
   my_float_AtStartup_DoNotUseDirectly,
  float, 0.0f
)

NEVER_PREF("my.string", String, "foo\\"bar")

NEVER_PREF("my.string2", String, "foobar")

ALWAYS_PREF(
  "my.atomic.bool",
   my_atomic_bool,
   my_atomic_bool,
  RelaxedAtomicBool, true
)

ALWAYS_DATAMUTEX_PREF(
  "my.datamutex.string",
   my_datamutex_string,
   my_datamutex_string,
  DataMutexString, "foobar"_ns
)

ALWAYS_DATAMUTEX_PREF(
  "my.datamutex.string.rust",
   my_datamutex_string_rust,
   my_datamutex_string_rust,
  DataMutexString, ""_ns
)

ALWAYS_PREF(
  "my.atomic.int",
   my_atomic_int,
   my_atomic_int_DoNotUseDirectly,
  ReleaseAcquireAtomicInt32, 10 + 10 * 20
)

ONCE_PREF(
  "my.atomic.uint",
   my_atomic_uint,
   my_atomic_uint_AtStartup,
  SequentiallyConsistentAtomicUint32, 68
)
""",
    "my_dashed": """\
// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT.

NEVER_PREF("my-dashed.atomic.float", AtomicFloat, 0.4455667)
""",
}

good["static_prefs_group_h"] = {
    "my": """\
// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT.
// Include it to gain access to StaticPrefs::my_*.

#ifndef mozilla_StaticPrefs_my_h
#define mozilla_StaticPrefs_my_h

#include "foobar.h"

#include "mozilla/StaticPrefListBegin.h"
#include "mozilla/StaticPrefList_my.h"
#include "mozilla/StaticPrefListEnd.h"

#endif  // mozilla_StaticPrefs_my_h
"""
}

good["static_prefs_c_getters_cpp"] = """\
// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT.

extern "C" uint32_t StaticPrefs_my_uint() {
  return mozilla::StaticPrefs::my_uint();
}

extern "C" float StaticPrefs_my_float_AtStartup_DoNotUseDirectly() {
  return mozilla::StaticPrefs::my_float_AtStartup_DoNotUseDirectly();
}

extern "C" bool StaticPrefs_my_atomic_bool() {
  return mozilla::StaticPrefs::my_atomic_bool();
}

extern "C" void StaticPrefs_my_datamutex_string_rust(nsACString *result) {
  const auto preflock = mozilla::StaticPrefs::my_datamutex_string_rust();
  result->Append(*preflock);
}
"""

good["static_prefs_rs"] = """\
// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT.

pub use nsstring::nsCString;
extern "C" {
    pub fn StaticPrefs_my_uint() -> u32;
    pub fn StaticPrefs_my_float_AtStartup_DoNotUseDirectly() -> f32;
    pub fn StaticPrefs_my_atomic_bool() -> bool;
    pub fn StaticPrefs_my_datamutex_string_rust(result: *mut nsstring::nsACString);
}

#[macro_export]
macro_rules! pref {
    ("my.uint") => (unsafe { $crate::StaticPrefs_my_uint() });
    ("my.float") => (unsafe { $crate::StaticPrefs_my_float_AtStartup_DoNotUseDirectly() });
    ("my.atomic.bool") => (unsafe { $crate::StaticPrefs_my_atomic_bool() });
    ("my.datamutex.string.rust") => (unsafe { let mut result = $crate::nsCString::new(); $crate::StaticPrefs_my_datamutex_string_rust(&mut *result); result });
}
"""

# A lot of bad inputs, each with an accompanying error message. Listed in order
# of the relevant `error` calls within generate_static_pref_list.py.
bad_inputs = [
    (
        """
- invalidkey: 3
""",
        "invalid key `invalidkey`",
    ),
    (
        """
- type: int32_t
""",
        "missing `name` key",
    ),
    (
        """
- name: 99
""",
        "non-string `name` value `99`",
    ),
    (
        """
- name: name_with_no_dot
""",
        "`name` value `name_with_no_dot` lacks a '.'",
    ),
    (
        """
- name: pref.is.defined.more.than.once
  type: bool
  value: false
  mirror: never
- name: pref.is.defined.more.than.once
  type: int32_t
  value: 111
  mirror: always
""",
        "`pref.is.defined.more.than.once` pref is defined more than once",
    ),
    (
        """
- name: your.pref
  type: bool
  value: false
  mirror: never
- name: my.pref
  type: bool
  value: false
  mirror: never
""",
        "`my.pref` pref must come before `your.pref` pref",
    ),
    (
        """
- name: missing.type.key
  value: false
  mirror: never
""",
        "missing `type` key for pref `missing.type.key`",
    ),
    (
        """
- name: invalid.type.value
  type: const char*
  value: true
  mirror: never
""",
        "invalid `type` value `const char*` for pref `invalid.type.value`",
    ),
    (
        """
- name: missing.value.key
  type: int32_t
  mirror: once
""",
        "missing `value` key for pref `missing.value.key`",
    ),
    (
        """
- name: non-string.value
  type: String
  value: 3.45
  mirror: once
""",
        "non-string `value` value `3.45` for `String` pref `non-string.value`; add double quotes",
    ),
    (
        """
- name: invalid.boolean.value
  type: bool
  value: true || false
  mirror: once
""",
        "invalid boolean value `true || false` for pref `invalid.boolean.value`",
    ),
    (
        """
- name: missing.mirror.key
  type: int32_t
  value: 3
""",
        "missing `mirror` key for pref `missing.mirror.key`",
    ),
    (
        """
- name: invalid.mirror.value
  type: bool
  value: true
  mirror: sometimes
""",
        "invalid `mirror` value `sometimes` for pref `invalid.mirror.value`",
    ),
    (
        """
- name: non-boolean.do_not_use_directly.value
  type: bool
  value: true
  mirror: always
  do_not_use_directly: 0
""",
        "non-boolean `do_not_use_directly` value `0` for pref "
        "`non-boolean.do_not_use_directly.value`",
    ),
    (
        """
- name: do_not_use_directly.uselessly.set
  type: int32_t
  value: 0
  mirror: never
  do_not_use_directly: true
""",
        "`do_not_use_directly` uselessly set with `mirror` value `never` for "
        "pref `do_not_use_directly.uselessly.set`",
    ),
    (
        """
- name: non-string.include.value
  type: bool
  value: true
  mirror: always
  include: 33
""",
        "non-string `include` value `33` for pref `non-string.include.value`",
    ),
    (
        """
- name: include.value.starts.with
  type: float
  value: M_PI
  mirror: never
  include: <cmath
""",
        "`include` value `<cmath` starts with `<` but does not end with `>` for "
        "pref `include.value.starts.with`",
    ),
    (
        """
- name: non-boolean.rust.value
  type: bool
  value: true
  mirror: always
  rust: 1
""",
        "non-boolean `rust` value `1` for pref `non-boolean.rust.value`",
    ),
    (
        """
- name: rust.uselessly.set
  type: int32_t
  value: 0
  mirror: never
  rust: true
""",
        "`rust` uselessly set with `mirror` value `never` for pref "
        "`rust.uselessly.set`",
    ),
]


class TestGenerateStaticPrefList(unittest.TestCase):
    """
    Unit tests for generate_static_pref_list.py.
    """

    def test_good(self):
        "Test various pieces of good input."
        inp = StringIO(good_input)
        pref_list = yaml.safe_load(inp)
        check_pref_list(pref_list)
        code = generate_code(pref_list, ["(string input)"])

        self.assertEqual(good["static_pref_list_all_h"], code["static_pref_list_all_h"])

        self.assertEqual(good["static_prefs_all_h"], code["static_prefs_all_h"])

        self.assertEqual(
            good["static_pref_list_group_h"]["my"],
            code["static_pref_list_group_h"]["my"],
        )
        self.assertEqual(
            good["static_pref_list_group_h"]["my_dashed"],
            code["static_pref_list_group_h"]["my_dashed"],
        )

        self.assertEqual(
            good["static_prefs_group_h"]["my"], code["static_prefs_group_h"]["my"]
        )

        self.assertEqual(
            good["static_prefs_c_getters_cpp"], code["static_prefs_c_getters_cpp"]
        )

        self.assertEqual(good["static_prefs_rs"], code["static_prefs_rs"])

    def test_bad(self):
        "Test various pieces of bad input."

        for input_string, expected in bad_inputs:
            inp = StringIO(input_string)
            try:
                pref_list = yaml.safe_load(inp)
                check_pref_list(pref_list)
                generate_code(pref_list, ["(string input"])
                self.assertEqual(0, 1)
            except ValueError as e:
                self.assertEqual(str(e), expected)


if __name__ == "__main__":
    mozunit.main()
