#
# Copyright (C) 2023 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.
#

import copy
import json
import textwrap
import unittest
from typing import Any

import gdbclient


class LaunchConfigMergeTest(unittest.TestCase):
    def merge_compare(self, base: dict[str, Any], to_add: dict[str, Any] | None, expected: dict[str, Any]) -> None:
        actual = copy.deepcopy(base)
        gdbclient.merge_launch_dict(actual, to_add)
        self.assertEqual(actual, expected, f'base={base}, to_add={to_add}')

    def test_add_none(self) -> None:
        base = { 'foo' : 1 }
        to_add = None
        expected = { 'foo' : 1 }
        self.merge_compare(base, to_add, expected)

    def test_add_val(self) -> None:
        base = { 'foo' : 1 }
        to_add = { 'bar' : 2}
        expected = { 'foo' : 1, 'bar' : 2 }
        self.merge_compare(base, to_add, expected)

    def test_overwrite_val(self) -> None:
        base = { 'foo' : 1 }
        to_add = { 'foo' : 2}
        expected = { 'foo' : 2 }
        self.merge_compare(base, to_add, expected)

    def test_lists_get_appended(self) -> None:
        base = { 'foo' : [1, 2] }
        to_add = { 'foo' : [3, 4]}
        expected = { 'foo' : [1, 2, 3, 4] }
        self.merge_compare(base, to_add, expected)

    def test_add_elem_to_dict(self) -> None:
        base = { 'foo' : { 'bar' : 1 } }
        to_add = { 'foo' : { 'baz' : 2 } }
        expected = { 'foo' : { 'bar' :  1, 'baz' : 2 } }
        self.merge_compare(base, to_add, expected)

    def test_overwrite_elem_in_dict(self) -> None:
        base = { 'foo' : { 'bar' : 1 } }
        to_add = { 'foo' : { 'bar' : 2 } }
        expected = { 'foo' : { 'bar' : 2 } }
        self.merge_compare(base, to_add, expected)

    def test_merging_dict_and_value_raises(self) -> None:
        base = { 'foo' : { 'bar' : 1 } }
        to_add = { 'foo' : 2 }
        with self.assertRaises(ValueError):
            gdbclient.merge_launch_dict(base, to_add)

    def test_merging_value_and_dict_raises(self) -> None:
        base = { 'foo' : 2 }
        to_add = { 'foo' : { 'bar' : 1 } }
        with self.assertRaises(ValueError):
            gdbclient.merge_launch_dict(base, to_add)

    def test_merging_dict_and_list_raises(self) -> None:
        base = { 'foo' : { 'bar' : 1 } }
        to_add = { 'foo' : [1] }
        with self.assertRaises(ValueError):
            gdbclient.merge_launch_dict(base, to_add)

    def test_merging_list_and_dict_raises(self) -> None:
        base = { 'foo' : [1] }
        to_add = { 'foo' : { 'bar' : 1 } }
        with self.assertRaises(ValueError):
            gdbclient.merge_launch_dict(base, to_add)

    def test_adding_elem_to_list_raises(self) -> None:
        base = { 'foo' : [1] }
        to_add = { 'foo' : 2}
        with self.assertRaises(ValueError):
            gdbclient.merge_launch_dict(base, to_add)

    def test_adding_list_to_elem_raises(self) -> None:
        base = { 'foo' : 1 }
        to_add = { 'foo' : [2]}
        with self.assertRaises(ValueError):
            gdbclient.merge_launch_dict(base, to_add)


class VsCodeLaunchGeneratorTest(unittest.TestCase):
    def setUp(self) -> None:
        # These tests can generate long diffs, so we remove the limit
        self.maxDiff = None

    def test_generate_script(self) -> None:
        self.assertEqual(json.loads(gdbclient.generate_vscode_lldb_script(root='/root',
                                                            sysroot='/sysroot',
                                                            binary_name='test',
                                                            port=123,
                                                            solib_search_path=['/path1',
                                                                               '/path2'],
                                                            extra_props=None)),
        {
             'name': '(lldbclient.py) Attach test (port: 123)',
             'type': 'lldb',
             'request': 'custom',
             'relativePathBase': '/root',
             'sourceMap': { '/b/f/w' : '/root', '': '/root', '.': '/root' },
             'initCommands': ['settings append target.exec-search-paths /path1 /path2'],
             'targetCreateCommands': ['target create test',
                                      'target modules search-paths add / /sysroot/'],
             'processCreateCommands': ['gdb-remote 123']
         })

    def test_generate_script_with_extra_props(self) -> None:
        extra_props = {
            'initCommands' : ['settings append target.exec-search-paths /path3'],
            'processCreateCommands' : ['break main', 'continue'],
            'sourceMap' : { '/test/' : '/root/test'},
            'preLaunchTask' : 'Build'
        }
        self.assertEqual(json.loads(gdbclient.generate_vscode_lldb_script(root='/root',
                                                            sysroot='/sysroot',
                                                            binary_name='test',
                                                            port=123,
                                                            solib_search_path=['/path1',
                                                                               '/path2'],
                                                            extra_props=extra_props)),
        {
             'name': '(lldbclient.py) Attach test (port: 123)',
             'type': 'lldb',
             'request': 'custom',
             'relativePathBase': '/root',
             'sourceMap': { '/b/f/w' : '/root',
                           '': '/root',
                           '.': '/root',
                           '/test/' : '/root/test' },
             'initCommands': [
                 'settings append target.exec-search-paths /path1 /path2',
                 'settings append target.exec-search-paths /path3',
             ],
             'targetCreateCommands': ['target create test',
                                      'target modules search-paths add / /sysroot/'],
             'processCreateCommands': ['gdb-remote 123',
                                       'break main',
                                       'continue'],
             'preLaunchTask' : 'Build'
         })


class LaunchConfigInsertTest(unittest.TestCase):
    def setUp(self) -> None:
        # These tests can generate long diffs, so we remove the limit
        self.maxDiff = None

    def test_insert_config(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-begin
            // #lldbclient-generated-end""")
        to_insert = textwrap.dedent("""\
                                    foo
                                    bar""")
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                            // #lldbclient-generated-begin
                            foo
                            bar
                            // #lldbclient-generated-end"""))

    def test_insert_into_start(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-begin
            // #lldbclient-generated-end
            more content""")
        to_insert = textwrap.dedent("""\
            foo
            bar""")
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                            // #lldbclient-generated-begin
                            foo
                            bar
                            // #lldbclient-generated-end
                            more content"""))

    def test_insert_into_mid(self) -> None:
        dst = textwrap.dedent("""\
            start content
            // #lldbclient-generated-begin
            // #lldbclient-generated-end
            more content""")
        to_insert = textwrap.dedent("""\
            foo
            bar""")
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                            start content
                            // #lldbclient-generated-begin
                            foo
                            bar
                            // #lldbclient-generated-end
                            more content"""))

    def test_insert_into_end(self) -> None:
        dst = textwrap.dedent("""\
            start content
            // #lldbclient-generated-begin
            // #lldbclient-generated-end""")
        to_insert = textwrap.dedent("""\
            foo
            bar""")
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                            start content
                            // #lldbclient-generated-begin
                            foo
                            bar
                            // #lldbclient-generated-end"""))

    def test_insert_twice(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-begin
            // #lldbclient-generated-end
            // #lldbclient-generated-begin
            // #lldbclient-generated-end
            """)
        to_insert = 'foo'
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                            // #lldbclient-generated-begin
                            foo
                            // #lldbclient-generated-end
                            // #lldbclient-generated-begin
                            foo
                            // #lldbclient-generated-end
                         """))

    def test_preserve_space_indent(self) -> None:
        dst = textwrap.dedent("""\
            {
              "version": "0.2.0",
              "configurations": [
                // #lldbclient-generated-begin
                // #lldbclient-generated-end
              ]
            }
        """)
        to_insert = textwrap.dedent("""\
            {
                "name": "(lldbclient.py) Attach test",
                "type": "lldb",
                "processCreateCommands": [
                    "gdb-remote 123",
                    "test"
                ]
            }""")
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                             {
                               "version": "0.2.0",
                               "configurations": [
                                 // #lldbclient-generated-begin
                                 {
                                     "name": "(lldbclient.py) Attach test",
                                     "type": "lldb",
                                     "processCreateCommands": [
                                         "gdb-remote 123",
                                         "test"
                                     ]
                                 }
                                 // #lldbclient-generated-end
                               ]
                             }
                         """))

    def test_preserve_tab_indent(self) -> None:
        dst = textwrap.dedent("""\
            {
            \t"version": "0.2.0",
            \t"configurations": [
            \t\t// #lldbclient-generated-begin
            \t\t// #lldbclient-generated-end
            \t]
            }
        """)
        to_insert = textwrap.dedent("""\
            {
            \t"name": "(lldbclient.py) Attach test",
            \t"type": "lldb",
            \t"processCreateCommands": [
            \t\t"gdb-remote 123",
            \t\t"test"
            \t]
            }""")
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                            {
                            \t"version": "0.2.0",
                            \t"configurations": [
                            \t\t// #lldbclient-generated-begin
                            \t\t{
                            \t\t\t"name": "(lldbclient.py) Attach test",
                            \t\t\t"type": "lldb",
                            \t\t\t"processCreateCommands": [
                            \t\t\t\t"gdb-remote 123",
                            \t\t\t\t"test"
                            \t\t\t]
                            \t\t}
                            \t\t// #lldbclient-generated-end
                            \t]
                            }
                         """))

    def test_preserve_trailing_whitespace(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-begin \t
            // #lldbclient-generated-end\t """)
        to_insert = 'foo'
        self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst,
                                                                      to_insert),
                         textwrap.dedent("""\
                            // #lldbclient-generated-begin \t
                            foo
                            // #lldbclient-generated-end\t """))

    def test_fail_if_no_begin(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-end""")
        with self.assertRaisesRegex(ValueError, 'Did not find begin marker line'):
            gdbclient.insert_commands_into_vscode_config(dst, 'foo')

    def test_fail_if_no_end(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-begin""")
        with self.assertRaisesRegex(ValueError, 'Unterminated begin marker at line 1'):
            gdbclient.insert_commands_into_vscode_config(dst, 'foo')

    def test_fail_if_begin_has_extra_text(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-begin text
            // #lldbclient-generated-end""")
        with self.assertRaisesRegex(ValueError, 'Did not find begin marker line'):
            gdbclient.insert_commands_into_vscode_config(dst, 'foo')

    def test_fail_if_end_has_extra_text(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-begin
            // #lldbclient-generated-end text""")
        with self.assertRaisesRegex(ValueError, 'Unterminated begin marker at line 1'):
            gdbclient.insert_commands_into_vscode_config(dst, 'foo')

    def test_fail_if_begin_end_swapped(self) -> None:
        dst = textwrap.dedent("""\
            // #lldbclient-generated-end
            // #lldbclient-generated-begin""")
        with self.assertRaisesRegex(ValueError, 'Unterminated begin marker at line 2'):
            gdbclient.insert_commands_into_vscode_config(dst, 'foo')


if __name__ == '__main__':
    unittest.main(verbosity=2)