#!/usr/bin/env python3

# Copyright © 2025 Pierre Le Marre <dev@wismill.eu>
# SPDX-License-Identifier: MIT

"""
This script generate tests for merge modes.
"""

from __future__ import annotations

from abc import ABCMeta, abstractmethod
import argparse
from collections import defaultdict
from collections.abc import Callable, Generator, Iterable, Iterator, Sequence
import dataclasses
import itertools
import re
import textwrap
from dataclasses import dataclass
from enum import Flag, IntFlag, StrEnum, auto, unique
from pathlib import Path
from string import Template
from typing import Any, ClassVar, NewType, Self

import jinja2

SCRIPT = Path(__file__)
# Root of the project
ROOT = SCRIPT.parent.parent

################################################################################
#
# XKB compiler
#
################################################################################


@dataclass
class XkbCompiler:
    name: str
    extended_syntax: bool

    default: ClassVar[str] = "xkbcommon"

    @property
    def is_default(self) -> bool:
        return self.name == self.default

    @property
    def suffix(self) -> str:
        return "" if self.is_default else "-" + self.name

    @classmethod
    def parse(cls, name: str) -> Self:
        match name:
            case cls.default:
                return cls(name=name, extended_syntax=True)
            case "xkbcomp":
                return cls(name=name, extended_syntax=False)
            case "kbvm":
                return cls(name=name, extended_syntax=True)
            case _:
                raise ValueError(f"Invalid XKB compiler: {name}")


################################################################################
#
# Merge modes
#
################################################################################


@unique
class MergeMode(StrEnum):
    Default = auto()
    Augment = auto()
    Override = auto()
    Replace = auto()

    @property
    def include(self) -> str:
        if self is self.__class__.Default:
            return "include"
        else:
            return self

    @property
    def char(self) -> str:
        match self:
            case self.__class__.Default:
                return "+"
            case self.__class__.Augment:
                return "|"
            case self.__class__.Override:
                return "+"
            case self.__class__.Replace:
                return "^"
            case _:
                raise ValueError(self)


INDENT = "\t"


################################################################################
#
# Components
#
################################################################################


@unique
class Component(StrEnum):
    keycodes = auto()
    types = auto()
    compat = auto()
    symbols = auto()

    def render_keymap(self, content) -> Generator[str]:
        yield "xkb_keymap {"
        for component in self.__class__:
            yield from (
                textwrap.indent(t, INDENT)
                for t in self.render(component, content if component is self else "")
            )
        yield "};"

    @classmethod
    def render(cls, component: Self, content: str, name: str = "") -> Generator[str]:
        if not content:
            match component:
                case cls.keycodes:
                    pass
                case cls.types:
                    pass
                case cls.compat:
                    content = "interpret.repeat= True;"
                case cls.symbols:
                    pass
        if name:
            name = f' "{name}"'
        if content:
            yield f'xkb_{component}{name} "" {{'
            yield textwrap.indent(content, INDENT)
            yield "};"
        else:
            yield f'xkb_{component}{name} "" {{}};'


class ComponentTemplate(Template):
    def __init__(self, template) -> Self:
        super().__init__(textwrap.dedent(template).strip())

    def __bool__(self) -> bool:
        return bool(self.template)

    def render(self, merge: MergeMode) -> str:
        mode = "" if merge is MergeMode.Default else f"{merge} "
        return self.substitute(mode=mode)

    def __add__(self, other: Any) -> Self:
        if isinstance(other, str):
            return self.__class__(self.template + "\n" + other)
        elif isinstance(other, Template):
            return self.__class__(self.template + "\n" + other.template)
        else:
            return NotImplemented

    def __radd__(self, other: Any) -> Self:
        if isinstance(other, str):
            return self.__class__(other + "\n" + self.template)
        elif isinstance(other, Template):
            return self.__class__(other.template + "\n" + self.template)
        else:
            return NotImplemented


################################################################################
#
# Test templates
#
################################################################################


@dataclass
class Test:
    title: str
    file: Path
    content: str
    expected: str


@dataclass
class TestGroup:
    title: str
    tests: Sequence[Test]

    c_template: ClassVar[Path] = Path("test/merge_modes.c.jinja")

    SLUG_PATTERN: ClassVar[re.Pattern[str]] = re.compile(r"[^-\w()]+")

    @classmethod
    def file(cls, title: str) -> Path:
        return Path(cls.SLUG_PATTERN.sub("-", title.lower().replace(" ", "_")))

    @classmethod
    def _write(
        cls,
        root: Path,
        jinja_env: jinja2.Environment,
        path: Path,
        **kwargs,
    ) -> None:
        template_path = root / cls.c_template
        template = jinja_env.get_template(template_path.relative_to(root).as_posix())

        with path.open("wt", encoding="utf-8") as fd:
            fd.writelines(template.generate(script=SCRIPT.relative_to(ROOT), **kwargs))

    @classmethod
    def write_c(
        cls,
        root: Path,
        jinja_env: jinja2.Environment,
        path: Path,
        compiler: XkbCompiler,
        tests: Sequence[TestGroup],
    ) -> None:
        return cls._write(
            root=root, jinja_env=jinja_env, path=path, compiler=compiler, tests=tests
        )

    @classmethod
    def write_data(
        cls,
        root: Path,
        compiler: XkbCompiler,
        tests: Sequence[TestGroup],
    ) -> None:
        tests_: dict[Path, list[Test]] = defaultdict(list)
        suffix = compiler.suffix
        for group in tests:
            for test in group.tests:
                file = test.file.parent
                file = file.with_name(file.name + suffix)
                tests_[file].append(test)
        for file, group_ in tests_.items():
            path = root / "test" / "data" / file
            with path.open("wt", encoding="utf-8") as fd:
                fd.write(
                    "// WARNING: This file was auto-generated by: "
                    f"{SCRIPT.relative_to(ROOT)}\n"
                )
                for test in group_:
                    fd.write(test.content)


@dataclass
class TestTemplate(metaclass=ABCMeta):
    title: str

    _plain: ClassVar[str] = "plain"
    _include: ClassVar[str] = "include"
    _base: ClassVar[str] = "base"
    _update: ClassVar[str] = "update"
    _file: ClassVar[str] = "merge_modes"

    def make_title(self, name: str) -> str:
        return f"{self.title}: {name}"

    @classmethod
    def make_file(cls, name: str) -> Path:
        return TestGroup.file(name)

    @abstractmethod
    def generate_tests(self) -> TestGroup: ...

    @abstractmethod
    def generate_data(self) -> TestGroup: ...

    @classmethod
    @abstractmethod
    def write(
        cls,
        root: Path,
        jinja_env: jinja2.Environment,
        c_file: Path | None,
        xkb: bool,
        compiler: XkbCompiler,
        tests: Sequence[Self],
        debug: bool,
    ): ...


@dataclass
class ComponentTestTemplate(TestTemplate):
    component: Component
    # Templates to test
    base_template: ComponentTemplate
    update_template: ComponentTemplate
    # Expected result
    augment: str
    override: str
    replace: str = ""

    def __post_init__(self):
        self.augment = textwrap.dedent(self.augment).strip()
        self.override = textwrap.dedent(self.override).strip()
        if not self.replace:
            self.replace = self.override
        else:
            self.replace = textwrap.dedent(self.replace).strip()

    def expected(self, mode: MergeMode) -> str:
        match mode:
            case MergeMode.Default:
                return self.override
            case MergeMode.Augment:
                return self.augment
            case MergeMode.Override:
                return self.override
            case MergeMode.Replace:
                return self.replace
            case _:
                raise ValueError(mode)

    def _make_section_name(self, type: str, mode: MergeMode) -> str:
        file = self.make_file(f"{self.title}-{mode}" if self.title else mode)
        return f"{type}-{file}"

    def _make_file(self, type: str, mode: MergeMode) -> Path:
        section_name = self._make_section_name(type, mode)
        return Path(self.component) / self._file / section_name

    def render_section(self, content: str, name: str = "") -> str:
        return "\n".join(self.component.render(self.component, content, name))

    def render_keymap(self, content: str) -> str:
        return "\n".join(self.component.render_keymap(content))

    def _generate_tests(
        self, render: Callable[[str], str], compiler: XkbCompiler
    ) -> Generator[Test]:
        include_suffix = compiler.suffix
        for base_mode, update_mode in itertools.product(MergeMode, MergeMode):
            # Plain
            content = (
                self.base_template.render(base_mode)
                + ("\n" if self.base_template else "")
                + self.update_template.render(update_mode)
            )
            yield Test(
                title=self.make_title(
                    f"{self._plain} ({base_mode}) - {self._plain} ({update_mode})"
                ),
                file=self.make_file(self.title) / self.make_file(update_mode),
                content=render(content),
                expected=render(self.expected(update_mode)),
            )

        for base_mode, update_mode in itertools.product(
            (MergeMode.Default,), MergeMode
        ):
            # Plain + Include
            file = self._make_file(self._update, update_mode)
            section = file.name
            file = file.parent.name
            for mode in MergeMode:
                yield Test(
                    title=self.make_title(
                        f"{self._plain} ({base_mode}) - {mode.include} ({update_mode})"
                    ),
                    file=self.make_file(self.title) / self.make_file(mode),
                    content=render(
                        self.base_template.render(base_mode)
                        + (
                            f'\n{mode.include} "{file}{include_suffix}({section})"'
                            if self.base_template
                            else ""
                        )
                    ),
                    # Include does not leak local merge modes
                    expected=render(self.expected(mode)),
                )

        # for base_mode, update_mode in itertools.product(MergeMode, MergeMode):
        #     # Include + Plain
        #     file = self._make_file(self.base, base_mode)
        #     section = file.name
        #     file = file.parent.name
        #     for mode in (MergeMode.default,):
        #         yield Test(
        #             title=self.make_title(
        #                 f"{mode.include} ({base_mode}) - {self.plain} ({update_mode})"
        #             ),
        #             file=self.make_file(self.title)
        #             / self.make_file(
        #                 f"{mode.include}({base_mode})-{self.plain}({update_mode})"
        #             ),
        #             content=render(
        #                 f'{mode.include} "{file}{include_suffix}({section})"\n'
        #                 + self.update_template.render(update_mode)
        #             ),
        #             expected=render(self.expected(update_mode)),
        #         )

        # for base_mode, update_mode in itertools.product((MergeMode.Default,), MergeMode):
        #     # Include + Include
        #     mode = MergeMode.Default
        #     base_file = self._make_file(self.base, mode)
        #     base_section = base_file.name
        #     base_file = base_file.parent.name
        #     update_file = self._make_file(self.update, mode)
        #     update_section = update_file.name
        #     update_file = update_file.parent.name
        #     yield Test(
        #         title=self.make_title(
        #             f"{base_mode.include} ({mode}) - {update_mode.include} ({mode})"
        #         ),
        #         file=self.make_file(self.title)
        #         / self.make_file(
        #             f"{base_mode.include}({mode})-{update_mode.include}({mode})"
        #         ),
        #         content=render(
        #             f'{base_mode.include} "{base_file}{include_suffix}({base_section})"\n'
        #             + f'{update_mode.include} "{update_file}{include_suffix}({update_section})"'
        #         ),
        #         expected=render(self.expected(update_mode)),
        #     )

        for base_mode, update_mode in itertools.product(MergeMode, MergeMode):
            # Multi-include
            base_file = self._make_file(self._base, base_mode)
            base_section = base_file.name
            base_file = base_file.parent.name
            update_file = self._make_file(self._update, update_mode)
            update_section = update_file.name
            update_file = update_file.parent.name
            for mode in (MergeMode.Override, MergeMode.Augment, MergeMode.Replace):
                yield Test(
                    title=self.make_title(
                        f"{self._include} ({base_mode} {mode.char} {update_mode})"
                    ),
                    file=self.make_file(self.title) / self.make_file(mode),
                    content=render(
                        f'{base_mode.include} "{base_file}{include_suffix}({base_section})'
                        + mode.char
                        + f'{update_file}{include_suffix}({update_section})"'
                    ),
                    expected=render(self.expected(mode)),
                )

    def _generate_data(self) -> Generator[Test]:
        for type, template in (
            (self._base, self.base_template),
            (self._update, self.update_template),
        ):
            for mode in MergeMode:
                section_name = self._make_section_name(type, mode)
                file = self._make_file(type, mode)
                content = template.render(mode)
                content = self.render_section(content=content, name=section_name) + "\n"
                yield Test(title="", file=file, content=content, expected="")

    def generate_tests(
        self, compiler: XkbCompiler, as_keymap: bool = False
    ) -> TestGroup:
        if as_keymap:

            def render(content: str) -> str:
                return self.render_keymap(content)
        else:

            def render(content: str) -> str:
                return self.render_section(content)

        return TestGroup(self.title, tuple(self._generate_tests(render, compiler)))

    def generate_data(self) -> TestGroup:
        return TestGroup(self.title, tuple(self._generate_data()))

    @classmethod
    def write(
        cls,
        root: Path,
        jinja_env: jinja2.Environment,
        c_file: Path | None,
        xkb: bool,
        compiler: XkbCompiler,
        tests: Sequence[Self],
        debug: bool = False,
    ):
        if c_file is not None and c_file.name:
            c_data = tuple(
                t.generate_tests(as_keymap=True, compiler=compiler) for t in tests
            )
            TestGroup.write_c(
                root=root,
                jinja_env=jinja_env,
                path=c_file,
                compiler=compiler,
                tests=c_data,
            )
        if xkb:
            xkb_data = tuple(t.generate_data() for t in tests)
            TestGroup.write_data(root=root, compiler=compiler, tests=xkb_data)


KeymapTemplate = Template(
    """\
xkb_keymap {
${keycodes}
${types}
${compat}
${symbols}
};"""
)


@dataclass
class KeymapTestTemplate(TestTemplate):
    keycodes: ComponentTestTemplate | None = None
    types: ComponentTestTemplate | None = None
    compat: ComponentTestTemplate | None = None
    symbols: ComponentTestTemplate | None = None

    def __post_init__(self):
        if self.keycodes:
            self.keycodes = dataclasses.replace(self.keycodes, title="")
        if self.types:
            self.types = dataclasses.replace(self.types, title="")
        if self.compat:
            self.compat = dataclasses.replace(self.compat, title="")
        if self.symbols:
            self.symbols = dataclasses.replace(self.symbols, title="")

    def __iter__(self) -> Generator[ComponentTestTemplate]:
        def make_empty_template(component: Component) -> ComponentTestTemplate:
            return ComponentTestTemplate(
                title="",
                component=component,
                base_template=ComponentTemplate(""),
                update_template=ComponentTemplate(""),
                augment="",
                override="",
            )

        yield self.keycodes or make_empty_template(Component.keycodes)
        yield self.types or make_empty_template(Component.types)
        yield self.compat or make_empty_template(Component.compat)
        yield self.symbols or make_empty_template(Component.symbols)

    def _generate_tests(self, compiler: XkbCompiler) -> Generator[Test]:
        groups = tuple(
            c.generate_tests(as_keymap=False, compiler=compiler) for c in self
        )
        keycodes: Test
        types: Test
        compat: Test
        symbols: Test
        for keycodes, types, compat, symbols in zip(*(g.tests for g in groups)):
            title = self.title + keycodes.title
            file = keycodes.file
            content = KeymapTemplate.substitute(
                keycodes=textwrap.indent(keycodes.content, INDENT),
                types=textwrap.indent(types.content, INDENT),
                compat=textwrap.indent(compat.content, INDENT),
                symbols=textwrap.indent(symbols.content, INDENT),
            )
            expected = KeymapTemplate.substitute(
                keycodes=textwrap.indent(keycodes.expected, INDENT),
                types=textwrap.indent(types.expected, INDENT),
                compat=textwrap.indent(compat.expected, INDENT),
                symbols=textwrap.indent(symbols.expected, INDENT),
            )
            yield Test(title=title, file=file, content=content, expected=expected)

    def generate_tests(self, compiler: XkbCompiler) -> TestGroup:
        return TestGroup(self.title, tuple(self._generate_tests(compiler)))

    def _generate_data(self) -> Generator[Test]:
        for component in self:
            yield from component.generate_data().tests

    def generate_data(self) -> TestGroup:
        return TestGroup(self.title, tuple(self._generate_data()))

    @classmethod
    def write(
        cls,
        root: Path,
        jinja_env: jinja2.Environment,
        c_file: Path | None,
        xkb: bool,
        compiler: XkbCompiler,
        tests: Sequence[Self],
        debug: bool = False,
    ):
        if c_file is not None and c_file.name:
            c_data = tuple(t.generate_tests(compiler) for t in tests)
            TestGroup.write_c(
                root=root,
                jinja_env=jinja_env,
                path=c_file,
                compiler=compiler,
                tests=c_data,
            )
        if xkb:
            xkb_data = tuple(t.generate_data() for t in tests)
            TestGroup.write_data(root=root, compiler=compiler, tests=xkb_data)


################################################################################
#
# Key types tests
#
################################################################################

TYPES_TESTS = ComponentTestTemplate(
    "Types",
    component=Component.types,
    base_template=ComponentTemplate(
        """
        ${mode}virtual_modifiers NumLock, LevelThree=none, LevelFive=Mod3;
        ${mode}virtual_modifiers U1, U2, U3;
        // ${mode}virtual_modifiers U4, U5, U6;
        ${mode}virtual_modifiers Z1=none, Z2=none, Z3=none;
        // ${mode}virtual_modifiers Z4=none, Z5=none, Z6=none;
        ${mode}virtual_modifiers M1=0x1000, M2=0x2000, M3=0x3000;
        // ${mode}virtual_modifiers M4=0x4000, M5=0x5000, M6=0x6000;

        ${mode}type "ONE_LEVEL" {
        	modifiers = None;
        	map[None] = Level1;
        	level_name[Level1]= "Any";
        };

        ${mode}type "TWO_LEVEL" {
        	modifiers = Shift;
        	map[Shift] = Level2;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        };

        ${mode}type "ALPHABETIC" {
        	modifiers = Shift+Lock;
        	map[Shift] = Level2;
        	map[Lock] = Level2;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Caps";
        };

        ${mode}type "KEYPAD" {
        	modifiers = Shift+NumLock;
        	map[None] = Level1;
        	map[Shift] = Level2;
        	map[NumLock] = Level2;
        	map[Shift+NumLock] = Level1;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Number";
        };

        ${mode}type "FOUR_LEVEL" {
        	modifiers = Shift+LevelThree;
        	map[None] = Level1;
        	map[Shift] = Level2;
        	map[LevelThree] = Level3;
        	map[Shift+LevelThree] = Level4;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        	level_name[Level3] = "Alt Base";
        	level_name[Level4] = "Shift Alt";
        };

        ${mode}type "FOUR_LEVEL_ALPHABETIC" {
        	modifiers = Shift+Lock+LevelThree;
        	map[None] = Level1;
        	map[Shift] = Level2;
        	map[Lock]  = Level2;
        	map[LevelThree] = Level3;
        	map[Shift+LevelThree] = Level4;
        	map[Lock+LevelThree] =  Level4;
        	map[Lock+Shift+LevelThree] =  Level3;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        	level_name[Level3] = "Alt Base";
        	level_name[Level4] = "Shift Alt";
        };

        ${mode}type "FOUR_LEVEL_SEMIALPHABETIC" {
        	modifiers = Shift+Lock+LevelThree;
        	map[None] = Level1;
        	map[Shift] = Level2;
        	map[Lock]  = Level2;
        	map[LevelThree] = Level3;
        	map[Shift+LevelThree] = Level4;
        	map[Lock+LevelThree] =  Level3;
        	map[Lock+Shift+LevelThree] = Level4;
        	preserve[Lock+LevelThree] = Lock;
        	preserve[Lock+Shift+LevelThree] = Lock;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        	level_name[Level3] = "Alt Base";
        	level_name[Level4] = "Shift Alt";
        };

        ${mode}type "XXX" {
        	modifiers = Shift+Lock+LevelThree;
            map[None]       = 1;
            map[Shift]      = 2;
            map[Lock]       = 2;
            map[LevelThree] = 3;
            level_name[1] = "1";
            level_name[2] = "2";
            level_name[3] = "3";
        };
        """
    ),
    update_template=ComponentTemplate(
        """\
        ${mode}virtual_modifiers NumLock=Mod2;    // Changed: now mapped
        ${mode}virtual_modifiers LevelThree=Mod5; // Changed: altered mapping (from 0)
        ${mode}virtual_modifiers LevelFive=Mod3;  // Unhanged: same mapping

        ${mode}virtual_modifiers U7, Z7=0, M7=0x700000; // Changed: new

        ${mode}virtual_modifiers U1;            // Unchanged (unmapped)
        ${mode}virtual_modifiers U2 = none;     // Changed: now mapped
        ${mode}virtual_modifiers U3 = 0x300000; // Changed: now mapped
        // ${mode}virtual_modifiers U4;            // Unchanged (unmapped)
        // ${mode}virtual_modifiers U5 = none;     // Changed: now mapped
        // ${mode}virtual_modifiers U6 = 0x600000; // Changed: now mapped
        ${mode}virtual_modifiers Z1;            // Unchanged (= 0)
        ${mode}virtual_modifiers Z2 = none;     // Unchanged (same mapping)
        ${mode}virtual_modifiers Z3 = 0x310000; // Changed: altered mapping (from 0)
        // ${mode}virtual_modifiers Z4;            // Unchanged (= 0)
        // ${mode}virtual_modifiers Z5 = none;     // Unchanged (same mapping)
        // ${mode}virtual_modifiers Z6 = 0x610000; // Changed: altered mapping (from 0)
        ${mode}virtual_modifiers M1;            // Unchanged (≠ 0)
        ${mode}virtual_modifiers M2 = none;     // Changed: reset
        ${mode}virtual_modifiers M3 = 0x320000; // Changed: altered mapping (from ≠ 0)
        // ${mode}virtual_modifiers M4;            // Unchanged (≠ 0)
        // ${mode}virtual_modifiers M5 = none;     // Changed: reset
        // ${mode}virtual_modifiers M6 = 0x620000; // Changed: altered mapping (from ≠ 0)

        ${mode}type "ONE_LEVEL" {
        	modifiers = None;
        	map[None] = Level1;
        	level_name[Level1]= "New"; // Change name
        };

        ${mode}type "TWO_LEVEL" {
        	modifiers = Shift+M1; // Changed
        	map[Shift] = Level2;
        	map[M1] = Level2; // Changed
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        };

        ${mode}type "ALPHABETIC" {
        	modifiers = Lock; // Changed
        	map[None] = Level2; // Changed
        	map[Lock] = Level1; // Changed
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Caps";
        };

        ${mode}type "KEYPAD" {
        	modifiers = Shift+NumLock;
        	map[None] = Level1;
        	map[Shift] = Level2; // Changed
        	map[NumLock] = Level2;
        	map[Shift+NumLock] = Level1;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Number";
        };

        // Unchanged
        ${mode}type "FOUR_LEVEL" {
        	modifiers = Shift+LevelThree;
        	map[None] = Level1;
        	map[Shift] = Level2;
        	map[LevelThree] = Level3;
        	map[Shift+LevelThree] = Level4;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        	level_name[Level3] = "Alt Base";
        	level_name[Level4] = "Shift Alt";
        };

        ${mode}type "FOUR_LEVEL_ALPHABETIC" {
        	modifiers = Shift+Lock+LevelThree;
        	map[None] = Level1;
        	map[Shift] = Level2;
        	map[Lock]  = Level2;
        	map[LevelThree] = Level3;
        	map[Shift+LevelThree] = Level4;
        	map[Lock+LevelThree] =  Level4;
        	map[Lock+Shift+LevelThree] =  Level3;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        	level_name[Level3] = "Alt Base";
        	level_name[Level4] = "Shift Alt";
        };

        ${mode}type "FOUR_LEVEL_SEMIALPHABETIC" {
        	modifiers = Shift+Lock+LevelThree;
        	map[None] = Level1;
        	map[Shift] = Level2;
        	map[Lock]  = Level2;
        	map[LevelThree] = Level3;
        	map[Shift+LevelThree] = Level4;
        	map[Lock+LevelThree] =  Level3;
        	map[Lock+Shift+LevelThree] = Level4;
        	preserve[Lock+LevelThree] = Lock;
        	preserve[Lock+Shift+LevelThree] = Lock;
        	level_name[Level1] = "Base";
        	level_name[Level2] = "Shift";
        	level_name[Level3] = "Alt Base";
        	level_name[Level4] = "Shift Alt";
        };

        ${mode}type "XXX" {
        	modifiers = Shift+Lock+LevelThree;
            map[None]       = 1;
            map[Shift]      = 2;
            // map[Lock]       = 2;    // Changed
            // map[LevelThree] = 3;
            map[LevelThree] = 1;       // Changed
            map[Shift+LevelThree] = 4; // Changed
            level_name[1] = "A"; // Changed
            level_name[2] = "2";
            level_name[3] = "3";
            level_name[4] = "4"; // Changed
        };

        // New
        ${mode}type "YYY" {
        	modifiers = None;
        	map[None] = Level1;
        	level_name[Level1]= "New";
        };
        """
    ),
    augment="""
        virtual_modifiers NumLock=Mod2,LevelThree=none,LevelFive=Mod3;

        virtual_modifiers U1,U2=0,U3=0x300000;
        // virtual_modifiers U4,U5=0,U6=0x600000;
        virtual_modifiers Z1=0,Z2=0,Z3=0;
        // virtual_modifiers Z4=0,Z5=0,Z6=0;
        virtual_modifiers M1=0x1000,M2=0x2000,M3=0x3000;
        // virtual_modifiers M4=0x4000,M5=0x5000,M6=0x6000;
        virtual_modifiers U7,Z7=0,M7=0x700000;

        type "ONE_LEVEL" {
        	modifiers= none;
            map[none]= 1;
        	level_name[1]= "Any";
        };
        type "TWO_LEVEL" {
        	modifiers= Shift;
        	map[Shift]= 2;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        };
        type "ALPHABETIC" {
        	modifiers= Shift+Lock;
        	map[Shift]= 2;
        	map[Lock]= 2;
        	level_name[1]= "Base";
        	level_name[2]= "Caps";
        };
        type "KEYPAD" {
        	modifiers= Shift+NumLock;
        	map[Shift]= 2;
        	map[NumLock]= 2;
        	level_name[1]= "Base";
        	level_name[2]= "Number";
        };
        type "FOUR_LEVEL" {
        	modifiers= Shift+LevelThree;
            map[none]= 1;
        	map[Shift]= 2;
        	map[LevelThree]= 3;
        	map[Shift+LevelThree]= 4;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        	level_name[3]= "Alt Base";
        	level_name[4]= "Shift Alt";
        };
        type "FOUR_LEVEL_ALPHABETIC" {
        	modifiers= Shift+Lock+LevelThree;
            map[none]= 1;
        	map[Shift]= 2;
        	map[Lock]= 2;
        	map[LevelThree]= 3;
        	map[Shift+LevelThree]= 4;
        	map[Lock+LevelThree]= 4;
        	map[Shift+Lock+LevelThree]= 3;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        	level_name[3]= "Alt Base";
        	level_name[4]= "Shift Alt";
        };
        type "FOUR_LEVEL_SEMIALPHABETIC" {
        	modifiers= Shift+Lock+LevelThree;
        	map[None]= 1;
        	map[Shift]= 2;
        	map[Lock]= 2;
        	map[LevelThree]= 3;
        	map[Shift+LevelThree]= 4;
        	map[Lock+LevelThree]= 3;
        	preserve[Lock+LevelThree]= Lock;
        	map[Shift+Lock+LevelThree]= 4;
        	preserve[Shift+Lock+LevelThree]= Lock;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        	level_name[3]= "Alt Base";
        	level_name[4]= "Shift Alt";
        };
        type "XXX" {
        	modifiers = Shift+Lock+LevelThree;
            map[None]       = 1;
            map[Shift]      = 2;
            map[Lock]       = 2;
            map[LevelThree] = 3;
            level_name[1] = "1";
            level_name[2] = "2";
            level_name[3] = "3";
        };
        type "YYY" {
        	modifiers = None;
        	map[None] = Level1;
        	level_name[Level1]= "New";
        };
        """,
    override="""
        virtual_modifiers NumLock=Mod2,LevelThree=Mod5,LevelFive=Mod3;

        virtual_modifiers U1,U2=0,U3=0x300000;
        // virtual_modifiers U4,U5=0,U6=0x600000;
        virtual_modifiers Z1=0,Z2=0,Z3=0x310000;
        // virtual_modifiers Z4=0,Z5=0,Z6=0x610000;
        virtual_modifiers M1=0x1000,M2=0,M3=0x320000;
        // virtual_modifiers M4=0x4000,M5=0,M6=0x620000;
        virtual_modifiers U7,Z7=0,M7=0x700000;

        type "ONE_LEVEL" {
        	modifiers= none;
        	map[None] = Level1;
        	level_name[1]= "New";
        };
        type "TWO_LEVEL" {
        	modifiers= Shift+M1;
        	map[Shift]= 2;
        	map[M1]= 2;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        };
        type "ALPHABETIC" {
        	modifiers= Lock;
        	map[none]= 2;
        	map[Lock]= 1;
        	level_name[1]= "Base";
        	level_name[2]= "Caps";
        };
        type "KEYPAD" {
        	modifiers= Shift+NumLock;
        	map[Shift] = Level2;
        	map[NumLock]= 2;
        	level_name[1]= "Base";
        	level_name[2]= "Number";
        };
        type "FOUR_LEVEL" {
        	modifiers= Shift+LevelThree;
        	map[None]= 1;
        	map[Shift]= 2;
        	map[LevelThree]= 3;
        	map[Shift+LevelThree] = Level4;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        	level_name[3]= "Alt Base";
        	level_name[4]= "Shift Alt";
        };
        type "FOUR_LEVEL_ALPHABETIC" {
        	modifiers= Shift+Lock+LevelThree;
        	map[Shift]= 2;
        	map[Lock]= 2;
        	map[LevelThree]= 3;
        	map[Shift+LevelThree]= 4;
        	map[Lock+LevelThree]= 4;
        	map[Shift+Lock+LevelThree]= 3;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        	level_name[3]= "Alt Base";
        	level_name[4]= "Shift Alt";
        };
        type "FOUR_LEVEL_SEMIALPHABETIC" {
        	modifiers= Shift+Lock+LevelThree;
        	map[None]= 1;
        	map[Shift]= 2;
        	map[Lock]= 2;
        	map[LevelThree]= 3;
        	map[Shift+LevelThree]= 4;
        	map[Lock+LevelThree]= 3;
        	preserve[Lock+LevelThree]= Lock;
        	map[Shift+Lock+LevelThree]= 4;
        	preserve[Shift+Lock+LevelThree]= Lock;
        	level_name[1]= "Base";
        	level_name[2]= "Shift";
        	level_name[3]= "Alt Base";
        	level_name[4]= "Shift Alt";
        };
        type "XXX" {
        	modifiers = Shift+Lock+LevelThree;
            map[None]       = 1;
            map[Shift]      = 2;
            // map[LevelThree] = 3;
            map[LevelThree] = 1;
            map[Shift+LevelThree] = 4;
            level_name[1] = "A";
            level_name[2] = "2";
            level_name[3] = "3";
            level_name[4] = "4";
        };
        type "YYY" {
        	modifiers = None;
        	map[None] = Level1;
        	level_name[Level1]= "New";
        };
        """,
)


################################################################################
#
# Compat tests
#
################################################################################

COMPAT_TESTS = ComponentTestTemplate(
    "Compat",
    component=Component.compat,
    base_template=ComponentTemplate(
        """
        ${mode}virtual_modifiers NumLock;

        interpret.repeat= False;
        setMods.clearLocks= True;
        latchMods.clearLocks= True;
        latchMods.latchToLock= True;

        ${mode}interpret Any + Any {
        	action= SetMods(modifiers=modMapMods);
        };

        ${mode}interpret Caps_Lock {
        	action = LockMods(modifiers = Lock);
        };

        ${mode}indicator "Caps Lock" {
        	!allowExplicit;
        	whichModState= Locked;
        	modifiers= Lock;
        };

        ${mode}indicator "Num Lock" {
        	!allowExplicit;
        	whichModState= Locked;
        	modifiers= NumLock;
        };
        """
    ),
    update_template=ComponentTemplate(
        """
        ${mode}virtual_modifiers NumLock;

        ${mode}interpret.repeat= False;
        ${mode}setMods.clearLocks= False; // Changed

        // Unchanged
        ${mode}interpret Any + Any {
        	action= SetMods(modifiers=modMapMods);
        };

        // Changed
        ${mode}interpret Caps_Lock {
        	action = LockMods(modifiers = NumLock);
        };

        // Unchanged
        ${mode}indicator "Caps Lock" {
        	!allowExplicit;
        	whichModState= Locked;
        	modifiers= Lock;
        };

        // Changed
        ${mode}indicator "Num Lock" {
        	!allowExplicit;
        	whichModState= Base;
        	modifiers= Lock;
        };

        // New
        ${mode}indicator "Kana" {
        	!allowExplicit;
        	whichModState= Locked;
        	modifiers= Control;
        };
        """
    ),
    augment="""
        virtual_modifiers NumLock;

        interpret.useModMapMods= AnyLevel;
        interpret.repeat= False;
        interpret Caps_Lock+AnyOfOrNone(all) {
        	action= LockMods(modifiers=Lock);
        };
        interpret Any+AnyOf(all) {
        	action= SetMods(modifiers=modMapMods,clearLocks);
        };
        indicator "Caps Lock" {
        	whichModState= locked;
        	modifiers= Lock;
        };
        indicator "Num Lock" {
        	whichModState= locked;
        	modifiers= NumLock;
        };
        indicator "Kana" {
        	whichModState= locked;
        	modifiers= Control;
	    };
        """,
    override="""
        virtual_modifiers NumLock;

        interpret.repeat= False;
        setMods.clearLocks= False;
        interpret Caps_Lock+AnyOfOrNone(all) {
        	action= LockMods(modifiers=NumLock);
        };
        interpret Any+AnyOf(all) {
        	action= SetMods(modifiers=modMapMods);
        };
        indicator "Caps Lock" {
        	whichModState= locked;
        	modifiers= Lock;
        };
        indicator "Kana" {
        	whichModState= locked;
        	modifiers= Control;
        };
        indicator "Num Lock" {
        	whichModState= base;
        	modifiers= Lock;
        };
        """,
)


################################################################################
#
# Symbols tests
#
################################################################################


LINUX_EVENT_CODES = (
    None,
    "ESC",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "0",
    "MINUS",
    "EQUAL",
    "BACKSPACE",
    "TAB",
    "Q",
    "W",
    "E",
    "R",
    "T",
    "Y",
    "U",
    "I",
    "O",
    "P",
    "LEFTBRACE",
    "RIGHTBRACE",
    "ENTER",
    "LEFTCTRL",
    "A",
    "S",
    "D",
    "F",
    "G",
    "H",
    "J",
    "K",
    "L",
    "SEMICOLON",
    "APOSTROPHE",
    "GRAVE",
    "LEFTSHIFT",
    "BACKSLASH",
    "Z",
    "X",
    "C",
    "V",
    "B",
    "N",
    "M",
    "COMMA",
    "DOT",
    "SLASH",
    "RIGHTSHIFT",
    "KPASTERISK",
    "LEFTALT",
    "SPACE",
    "CAPSLOCK",
    "F1",
    "F2",
    "F3",
    "F4",
    "F5",
    "F6",
    "F7",
    "F8",
    "F9",
    "F10",
    "NUMLOCK",
    "SCROLLLOCK",
    "KP7",
    "KP8",
    "KP9",
    "KPMINUS",
    "KP4",
    "KP5",
    "KP6",
    "KPPLUS",
    "KP1",
    "KP2",
    "KP3",
    "KP0",
    "KPDOT",
    None,
    "ZENKAKUHANKAKU",
    "102ND",
    "F11",
    "F12",
    "RO",
    "KATAKANA",
    "HIRAGANA",
    "HENKAN",
    "KATAKANAHIRAGANA",
    "MUHENKAN",
    "KPJPCOMMA",
    "KPENTER",
    "RIGHTCTRL",
    "KPSLASH",
    "SYSRQ",
    "RIGHTALT",
    "LINEFEED",
    "HOME",
    "UP",
    "PAGEUP",
    "LEFT",
    "RIGHT",
    "END",
    "DOWN",
    "PAGEDOWN",
    "INSERT",
    "DELETE",
    "MACRO",
    "MUTE",
    "VOLUMEDOWN",
    "VOLUMEUP",
    "POWER",
    "KPEQUAL",
    "KPPLUSMINUS",
    "PAUSE",
    "SCALE",
    "KPCOMMA",
    "HANGEUL",
    "HANJA",
    "YEN",
    "LEFTMETA",
    "RIGHTMETA",
    "COMPOSE",
    "STOP",
    "AGAIN",
    "PROPS",
    "UNDO",
    "FRONT",
    "COPY",
    "OPEN",
    "PASTE",
    "FIND",
    "CUT",
    "HELP",
    "MENU",
    "CALC",
    "SETUP",
    "SLEEP",
    "WAKEUP",
    "FILE",
    "SENDFILE",
    "DELETEFILE",
    "XFER",
    "PROG1",
    "PROG2",
    "WWW",
    "MSDOS",
    "COFFEE",
    "DIRECTION",
    "CYCLEWINDOWS",
    "MAIL",
    "BOOKMARKS",
    "COMPUTER",
    "BACK",
    "FORWARD",
    "CLOSECD",
    "EJECTCD",
    "EJECTCLOSECD",
    "NEXTSONG",
    "PLAYPAUSE",
    "PREVIOUSSONG",
    "STOPCD",
    "RECORD",
    "REWIND",
    "PHONE",
    "ISO",
    "CONFIG",
    "HOMEPAGE",
    "REFRESH",
    "EXIT",
    "MOVE",
    "EDIT",
    "SCROLLUP",
    "SCROLLDOWN",
    "KPLEFTPAREN",
    "KPRIGHTPAREN",
    "NEW",
    "REDO",
    "F13",
    "F14",
    "F15",
    "F16",
    "F17",
    "F18",
    "F19",
    "F20",
    "F21",
    "F22",
    "F23",
    "F24",
    None,
    None,
    None,
    None,
    None,
    "PLAYCD",
    "PAUSECD",
    "PROG3",
    "PROG4",
    "DASHBOARD",
    "SUSPEND",
    "CLOSE",
    "PLAY",
    "FASTFORWARD",
    "BASSBOOST",
    "PRINT",
    "HP",
    "CAMERA",
    "SOUND",
    "QUESTION",
)

Comment = NewType("Comment", str)


@dataclass(frozen=True)
class KeyCode:
    _evdev: str
    _xkb: str

    @property
    def c(self) -> str:
        return f"KEY_{self._evdev}"

    @property
    def xkb(self) -> str:
        return f"<{self._xkb}>"


class Keysym(str):
    @property
    def c(self) -> str:
        return f"XKB_KEY_{self}"

    @classmethod
    def parse(cls, raw: str | None) -> Self:
        if not raw:
            return cls("NoSymbol")
        else:
            return cls(raw)


NoSymbol = Keysym("NoSymbol")


class Modifier(IntFlag):
    NoModifier = 0
    Shift = 1 << 0
    Lock = 1 << 1
    Control = 1 << 2
    Mod1 = 1 << 3
    Mod2 = 1 << 4
    Mod3 = 1 << 5
    Mod4 = 1 << 6
    Mod5 = 1 << 7
    LevelThree = Mod5

    def __iter__(self) -> Iterator[Self]:
        for m in self.__class__:
            if m & self:
                yield m

    def __str__(self) -> str:
        return "+".join(m.name for m in self)


class Action(metaclass=ABCMeta):
    @abstractmethod
    def __bool__(self) -> bool: ...

    @classmethod
    def parse(cls, raw: Any) -> Self:
        if raw is None:
            return GroupAction.parse(None)
        elif isinstance(raw, Modifier):
            return ModAction.parse(raw)
        elif isinstance(raw, int):
            return GroupAction.parse(raw)
        else:
            raise ValueError(raw)

    @abstractmethod
    def action_to_keysym(self, index: int, level: int) -> Keysym: ...


@dataclass
class GroupAction(Action):
    """
    SetGroup or NoAction
    """

    group: int
    keysyms: ClassVar[dict[tuple[int, int, int], Keysym]] = {
        (2, 0, 0): Keysym("a"),
        (2, 0, 1): Keysym("A"),
        (2, 1, 0): Keysym("b"),
        (2, 1, 1): Keysym("B"),
        (3, 0, 0): Keysym("Greek_alpha"),
        (3, 0, 1): Keysym("Greek_ALPHA"),
        (3, 1, 0): Keysym("Greek_beta"),
        (3, 1, 1): Keysym("Greek_BETA"),
    }

    def __str__(self) -> str:
        if self.group > 0:
            return f"SetGroup(group={self.group})"
        else:
            return "NoAction()"

    def __bool__(self) -> bool:
        return bool(self.group)

    @classmethod
    def parse(cls, raw: int | None) -> Self:
        if not raw:
            return cls(0)
        else:
            return cls(raw)

    def action_to_keysym(self, index: int, level: int) -> Keysym:
        if not self.group:
            return NoSymbol
        else:
            if (
                keysym := self.keysyms.get((self.group % 4, index % 2, level % 2))
            ) is None:
                raise ValueError((self, index, level))
            return keysym


@dataclass
class ModAction(Action):
    """
    SetMod or NoAction
    """

    mods: Modifier
    keysyms: ClassVar[dict[tuple[Modifier, int, int], Keysym]] = {
        (Modifier.Control, 0, 0): Keysym("x"),
        (Modifier.Control, 0, 1): Keysym("X"),
        (Modifier.Control, 1, 0): Keysym("y"),
        (Modifier.Control, 1, 1): Keysym("Y"),
        (Modifier.Mod5, 0, 0): Keysym("Greek_xi"),
        (Modifier.Mod5, 0, 1): Keysym("Greek_XI"),
        (Modifier.Mod5, 1, 0): Keysym("Greek_upsilon"),
        (Modifier.Mod5, 1, 1): Keysym("Greek_UPSILON"),
    }

    def __str__(self) -> str:
        if self.mods is Modifier.NoModifier:
            return "NoAction()"
        else:
            return f"SetMods(mods={self.mods})"

    def __bool__(self) -> bool:
        return self.mods is Modifier.NoModifier

    @classmethod
    def parse(cls, raw: Modifier | None) -> Self:
        if not raw:
            return cls(Modifier.NoModifier)
        else:
            return cls(raw)

    def action_to_keysym(self, index: int, level: int) -> Keysym:
        if self.mods is Modifier.NoModifier:
            return NoSymbol
        else:
            if (keysym := self.keysyms.get((self.mods, index % 2, level % 2))) is None:
                raise ValueError((self, index, level))
            return keysym


@dataclass
class Level:
    keysyms: tuple[Keysym, ...]
    actions: tuple[Action, ...]

    @staticmethod
    def _c(default: str, xs: Iterable[Any]) -> str:
        match len(xs):
            case 0:
                return default
            case 1:
                return xs[0].c
            case _:
                return ", ".join(map(lambda x: x.c, xs))

    @staticmethod
    def _xkb(default: str, xs: Iterable[Any]) -> str:
        match len(xs):
            case 0:
                return default
            case 1:
                return str(xs[0])
            case _:
                return "{" + ", ".join(map(str, xs)) + "}"

    @classmethod
    def has_empty_symbols(cls, keysyms: tuple[Keysym, ...]) -> bool:
        return all(ks == NoSymbol for ks in keysyms)

    @property
    def empty_symbols(self) -> bool:
        return self.has_empty_symbols(self.keysyms)

    @property
    def keysyms_c(self) -> str:
        if not self.keysyms and self.actions:
            return self._c(
                NoSymbol.c, tuple(itertools.repeat(NoSymbol, len(self.actions)))
            )
        return self._c(NoSymbol.c, self.keysyms)

    @property
    def keysyms_xkb(self) -> str:
        return self._xkb(NoSymbol, self.keysyms)

    @classmethod
    def has_empty_actions(cls, actions: tuple[Action, ...]) -> bool:
        return not any(actions)

    @property
    def empty_actions(self) -> bool:
        return self.has_empty_actions(self.actions)

    @property
    def actions_xkb(self) -> str:
        return self._xkb("NoAction()", self.actions)

    @classmethod
    def Keysyms(cls, *keysyms: str | None) -> Self:
        return cls.Mix(keysyms, ())

    @classmethod
    def Actions(cls, *actions: int | Modifier | None) -> Self:
        return cls.Mix((), actions)

    @classmethod
    def Mix(
        cls, keysyms: tuple[str | None, ...], actions: tuple[int | Modifier | None,]
    ) -> Self:
        return cls(tuple(map(Keysym.parse, keysyms)), tuple(map(Action.parse, actions)))

    def add_keysyms(self, keep_actions: bool, level: int) -> Self:
        return self.__class__(
            keysyms=tuple(
                a.action_to_keysym(index=k, level=level)
                for k, a in enumerate(self.actions)
            ),
            actions=self.actions if keep_actions else (),
        )

    @property
    def target_group(self) -> int:
        for a in self.actions:
            if isinstance(a, GroupAction) and a.group > 1:
                return a.group
        else:
            return 0

    @property
    def target_level(self) -> int:
        for a in self.actions:
            if isinstance(a, ModAction) and a.mods:
                match a.mods:
                    case Modifier.LevelThree:
                        return 2
                    case _:
                        return 0
        else:
            return 0


@dataclass
class KeyEntry:
    levels: tuple[Level, ...]

    def __init__(self, *levels: Level):
        self.levels = levels

    @property
    def xkb(self) -> Iterator[str]:
        if not self.levels:
            yield ""
            return
        keysyms = tuple(l.keysyms for l in self.levels)
        has_keysyms = any(not Level.has_empty_symbols(s) for s in keysyms)
        no_keysyms = all(not s for s in keysyms)
        actions = tuple(l.actions for l in self.levels)
        has_actions = any(not Level.has_empty_actions(a) for a in actions)
        if has_keysyms or (not no_keysyms and not has_actions):
            yield "["
            yield ", ".join(l.keysyms_xkb for l in self.levels)
            yield "]"
        if has_actions or no_keysyms:
            if has_keysyms:
                yield ", "
            yield "["
            yield ", ".join(l.actions_xkb for l in self.levels)
            yield "]"

    def add_keysyms(self, keep_actions: bool) -> Self:
        return self.__class__(
            *(
                l.add_keysyms(keep_actions=keep_actions, level=k)
                for k, l in enumerate(self.levels)
            )
        )


class TestType(IntFlag):
    KeysymsOnly = auto()
    ActionsOnly = auto()
    KeysymsAndActions = auto()
    All = ActionsOnly | KeysymsOnly | KeysymsAndActions


@dataclass
class TestId:
    type: TestType
    base: int = 0
    _last_base: ClassVar[int] = 0
    _max_type: ClassVar[int] = TestType.KeysymsAndActions >> 1
    _forbidden_keys: tuple[str, ...] = ("LEFTSHIFT", "RIGHTALT")

    def __post_init__(self):
        if self.base == -1:
            self.base = self.__class__._last_base
        elif self.base < 0:
            raise ValueError(self.base)
        elif self.base == 0:
            self.__class__._last_base += 1
            self.base = self.__class__._last_base
            while not self.check_linux_keys(self._base_id):
                self.__class__._last_base += 1
                self.base = self.__class__._last_base
        else:
            self.__class__._last_base = max(self.__class__._last_base, self.base)
        assert self.linux_key, self
        # print(self.base, self.id, self.xkb_key)

    def __hash__(self) -> int:
        return self.id

    @property
    def _base_id(self) -> int:
        return (self.base - 1) * (self._max_type + 1) + 1

    @property
    def id(self) -> int:
        # return (self.base << 2) | (self.type >> 1)
        return self._base_id + (self.type >> 1)

    def with_type(self, type: TestType) -> Self:
        return dataclasses.replace(self, type=type)

    @property
    def xkb_key(self) -> str:
        return f"T{self.id:0>3}"

    @classmethod
    def check_linux_key(cls, idx: int) -> bool:
        if idx >= len(LINUX_EVENT_CODES):
            raise ValueError("Not enough Linux keys available!")
        key: str | None = LINUX_EVENT_CODES[idx]
        return bool(key) and key not in cls._forbidden_keys

    @classmethod
    def check_linux_keys(cls, base_id) -> bool:
        return all(
            cls.check_linux_key(base_id + k) for k in range(0, cls._max_type + 1)
        )

    @property
    def linux_key(self) -> str:
        if self.id >= len(LINUX_EVENT_CODES):
            raise ValueError("Not enough Linux keys available!")
        return LINUX_EVENT_CODES[self.id]

    @property
    def key(self) -> KeyCode:
        return KeyCode(self.linux_key, self.xkb_key)


@unique
class Implementation(Flag):
    x11 = auto()
    xkbcommon = auto()
    all = x11 | xkbcommon


@dataclass
class TestEntry:
    id: TestId
    key: KeyCode = dataclasses.field(init=False)
    base: KeyEntry
    update: KeyEntry
    augment: KeyEntry
    override: KeyEntry
    replace: KeyEntry
    types: TestType
    implementations: Implementation

    group_keysyms: ClassVar[tuple[tuple[str, str], ...]] = (
        ("Ukrainian_i", "Ukrainian_I", "Ukrainian_yi", "Ukrainian_YI"),
        ("ch", "Ch", "c_h", "C_h"),
    )

    def __init__(
        self,
        id: TestId | None,
        base: KeyEntry,
        update: KeyEntry,
        augment: KeyEntry,
        override: KeyEntry,
        types: TestType = TestType.All,
        replace: KeyEntry | None = None,
        implementations: Implementation = Implementation.all,
    ):
        self.id = TestId(0, 0) if id is None else id
        self.key = id.key
        self.base = base
        self.update = update
        self.augment = augment
        self.override = override
        self.types = types
        self.replace = self.update if replace is None else replace
        self.implementations = implementations

    @property
    def default(self) -> KeyEntry:
        return self.override

    def add_keysyms(self, keep_actions: bool) -> Self:
        types = TestType.KeysymsAndActions if keep_actions else TestType.KeysymsOnly
        return dataclasses.replace(
            self,
            id=self.id.with_type(types),
            base=self.base.add_keysyms(keep_actions=keep_actions),
            update=self.update.add_keysyms(keep_actions=keep_actions),
            augment=self.augment.add_keysyms(keep_actions=keep_actions),
            override=self.override.add_keysyms(keep_actions=keep_actions),
            replace=self.replace.add_keysyms(keep_actions=keep_actions),
            types=types,
        )

    @classmethod
    def alt_keysym(cls, group: int, level: int) -> Keysym:
        return Keysym(cls.group_keysyms[group % 2][level % 4])

    @classmethod
    def alt_keysyms(cls, group: int) -> Iterator[Keysym]:
        for keysym in cls.group_keysyms[group % 2]:
            yield Keysym(keysym)


@dataclass
class SymbolsTestGroup:
    name: str
    tests: tuple[TestEntry | Comment, ...]

    def _with_implementation(
        self, implementation: Implementation
    ) -> Iterable[TestEntry | Comment]:
        pending_comment: Comment | None = None
        for t in self.tests:
            if not isinstance(t, TestEntry):
                pending_comment = t
            elif t.implementations & implementation:
                if pending_comment is not None:
                    yield pending_comment
                    pending_comment = None
                yield t

    def with_implementation(self, implementation: Implementation) -> Self:
        return dataclasses.replace(
            self, tests=tuple(self._with_implementation(implementation))
        )

    @staticmethod
    def _add_keysyms(entry: TestEntry | Comment) -> Iterable[TestEntry | Comment]:
        if isinstance(entry, TestEntry) and entry.id.type is TestType.ActionsOnly:
            if entry.types & TestType.KeysymsOnly:
                yield entry.add_keysyms(keep_actions=False)
            yield entry
            if entry.types & TestType.KeysymsAndActions:
                yield entry.add_keysyms(keep_actions=True)
        else:
            yield entry

    def add_keysyms(self, name: str = "") -> Self:
        return dataclasses.replace(
            self,
            name=name or self.name,
            tests=tuple(t for ts in self.tests for t in self._add_keysyms(ts)),
        )

    def __add__(self, other: Any) -> Self:
        if isinstance(other, tuple):
            return dataclasses.replace(self, tests=self.tests + other)
        elif isinstance(other, self.__class__):
            return dataclasses.replace(self, tests=self.tests + other.tests)
        else:
            return NotImplemented

    def get_keycodes(self, base: ComponentTestTemplate) -> ComponentTestTemplate:
        ids: set[TestId] = set(t.id for t in self.tests if isinstance(t, TestEntry))

        base_template = Template(
            "\n".join(f"${{mode}}<{id.xkb_key}> = {id.id + 8};" for id in ids) + "\n"
        )
        keycodes = base_template.substitute(mode="")

        return dataclasses.replace(
            base,
            base_template=base_template + base.base_template,
            update_template=base.update_template,
            augment=keycodes + base.augment,
            override=keycodes + base.override,
            replace=keycodes + base.replace,
        )

    def _get_symbols(self, type: str, debug: bool) -> Generator[str]:
        for entry in self.tests:
            if isinstance(entry, str):
                if type == "update":
                    yield f"\n////// {entry} //////"
            else:
                data: KeyEntry = getattr(entry, type)
                if debug:
                    yield f"// base:     {''.join(entry.base.xkb) or '(empty)'}"
                    yield "// key to merge / replace result"
                yield f"${{mode}}key {entry.key.xkb} {{ {''.join(data.xkb)} }};"
                if debug:
                    yield f"// override: {''.join(entry.override.xkb) or '(empty)'}"
                    yield f"// augment:  {''.join(entry.augment.xkb) or '(empty)'}"

    def get_symbols(self, debug: bool = False) -> ComponentTestTemplate:
        base_template = ComponentTemplate("\n".join(self._get_symbols("base", False)))
        update_template = ComponentTemplate(
            "\n".join(self._get_symbols("update", debug))
        )
        augment = ComponentTemplate(
            "\n".join(self._get_symbols("augment", False))
        ).substitute(mode="")
        override = ComponentTemplate(
            "\n".join(self._get_symbols("override", False))
        ).substitute(mode="")
        replace = ComponentTemplate(
            "\n".join(self._get_symbols("replace", False))
        ).substitute(mode="")
        return ComponentTestTemplate(
            title=self.name,
            component=Component.symbols,
            base_template=base_template,
            update_template=update_template,
            augment=augment,
            override=override,
            replace=replace,
        )


SYMBOLS_TESTS_BOTH = SymbolsTestGroup(
    "",
    (
        Comment("Trivial cases"),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(),
            update=KeyEntry(),
            augment=KeyEntry(),
            override=KeyEntry(),
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(),
            update=KeyEntry(Level.Actions(3)),
            augment=KeyEntry(Level.Actions(3)),
            override=KeyEntry(Level.Actions(3)),
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2)),
            update=KeyEntry(),
            augment=KeyEntry(Level.Actions(2)),
            override=KeyEntry(Level.Actions(2)),
        ),
        Comment("Same key"),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2)),
            update=KeyEntry(Level.Actions(2)),
            augment=KeyEntry(Level.Actions(2)),
            override=KeyEntry(Level.Actions(2)),
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
            override=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Mismatch levels count"),
        (
            TEST_BOTH_Q := TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(2)),
                update=KeyEntry(
                    Level.Actions(3), Level.Actions(None), Level.Actions(None)
                ),
                augment=KeyEntry(
                    Level.Actions(3), Level.Actions(2), Level.Actions(None)
                ),
                override=KeyEntry(
                    Level.Actions(3), Level.Actions(2), Level.Actions(None)
                ),
                # X11 and xkbcommon handle keysyms-only case differently
                types=TestType.ActionsOnly | TestType.KeysymsAndActions,
            )
        ),
        # Trailing NoSymbols are discarded in xkbcomp
        dataclasses.replace(
            TEST_BOTH_Q,
            augment=KeyEntry(Level.Actions(3), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3), Level.Actions(2)),
            replace=KeyEntry(Level.Actions(3)),
            implementations=Implementation.x11,
        ).add_keysyms(keep_actions=False),
        dataclasses.replace(
            TEST_BOTH_Q, implementations=Implementation.xkbcommon
        ).add_keysyms(keep_actions=False),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3), Level.Actions(None), Level.Actions(None)),
            augment=KeyEntry(Level.Actions(3), Level.Actions(2), Level.Actions(None)),
            override=KeyEntry(Level.Actions(3), Level.Actions(2), Level.Actions(None)),
            # X11 and xkbcommon handle keysyms-only case differently
            types=TestType.ActionsOnly | TestType.KeysymsAndActions,
        ),
        (
            TEST_BOTH_W := TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(2), Level.Actions(None)),
                update=KeyEntry(Level.Actions(3), Level.Actions(None)),
                augment=KeyEntry(
                    Level.Actions(3), Level.Actions(2), Level.Actions(None)
                ),
                override=KeyEntry(
                    Level.Actions(3), Level.Actions(2), Level.Actions(None)
                ),
                # X11 and xkbcommon handle keysyms-only case differently
                types=TestType.ActionsOnly | TestType.KeysymsAndActions,
            )
        ),
        dataclasses.replace(
            TEST_BOTH_W,
            augment=KeyEntry(Level.Actions(3), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3), Level.Actions(2)),
            replace=KeyEntry(Level.Actions(3)),
            implementations=Implementation.x11,
        ).add_keysyms(keep_actions=False),
        dataclasses.replace(
            TEST_BOTH_W, implementations=Implementation.xkbcommon
        ).add_keysyms(keep_actions=False),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(3)),
            override=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(3)),
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3), Level.Actions(3), Level.Actions(2)),
        ),
        Comment("Single keysyms -> single keysyms"),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(None), Level.Actions(None)),
            augment=KeyEntry(Level.Actions(None), Level.Actions(None)),
            override=KeyEntry(Level.Actions(None), Level.Actions(None)),
        ),
        (
            TEST_BOTH_Y := TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(None), Level.Actions(None)),
                update=KeyEntry(Level.Actions(3), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(3), Level.Actions(None)),
                override=KeyEntry(Level.Actions(3), Level.Actions(None)),
                # X11 and xkbcommon handle keysyms-only case differently
                types=TestType.ActionsOnly | TestType.KeysymsAndActions,
            )
        ),
        dataclasses.replace(
            TEST_BOTH_Y,
            augment=KeyEntry(Level.Actions(3)),
            override=KeyEntry(Level.Actions(3)),
            replace=KeyEntry(Level.Actions(3)),
            implementations=Implementation.x11,
        ).add_keysyms(keep_actions=False),
        dataclasses.replace(
            TEST_BOTH_Y, implementations=Implementation.xkbcommon
        ).add_keysyms(keep_actions=False),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(None), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(None), Level.Actions(3)),
            override=KeyEntry(Level.Actions(None), Level.Actions(3)),
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(3), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(3), Level.Actions(3)),
            override=KeyEntry(Level.Actions(3), Level.Actions(3)),
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(None), Level.Actions(None)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(2), Level.Actions(2)),
        ),
        (
            TEST_BOTH_P := TestEntry(
                TestId(TestType.ActionsOnly),
                KeyEntry(Level.Actions(2), Level.Actions(2)),
                update=KeyEntry(Level.Actions(3), Level.Actions(None)),
                augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
                override=KeyEntry(Level.Actions(3), Level.Actions(2)),
                # X11 and xkbcommon handle keysyms-only case differently
                types=TestType.ActionsOnly | TestType.KeysymsAndActions,
            )
        ),
        dataclasses.replace(
            TEST_BOTH_P,
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3), Level.Actions(2)),
            replace=KeyEntry(Level.Actions(3)),
            implementations=Implementation.x11,
        ).add_keysyms(keep_actions=False),
        dataclasses.replace(
            TEST_BOTH_P, implementations=Implementation.xkbcommon
        ).add_keysyms(keep_actions=False),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(None), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(2), Level.Actions(3)),
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3), Level.Actions(3)),
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Keysyms("a"), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3), Level.Keysyms("X")),
            augment=KeyEntry(Level.Mix(("a"), (3,)), Level.Mix(("X",), (2,))),
            override=KeyEntry(Level.Mix(("a"), (3,)), Level.Mix(("X",), (2,))),
        ),
        Comment("Single keysyms -> multiple keysyms"),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
            augment=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(None, None)),
            augment=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
            augment=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
            override=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(None, None), Level.Actions(3, None)),
            augment=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
            override=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
            augment=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None), Level.Actions(None)),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            augment=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            override=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(None)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(2)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(None, None)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(2)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(None), Level.Actions(3, None)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(2), Level.Actions(3, None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(None, None), Level.Actions(3, None)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(2), Level.Actions(3, None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(3, None)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2), Level.Actions(2)),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2)),
            override=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Multiple keysyms -> multiple keysyms"),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None, None), Level.Actions(None, None)),
            update=KeyEntry(
                Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
            ),
            augment=KeyEntry(
                Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
            ),
            override=KeyEntry(
                Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None, None), Level.Actions(None, None)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
            augment=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            augment=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(2, Modifier.Control),
            ),
            override=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
            augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(Modifier.LevelThree, 3),
            ),
            augment=KeyEntry(
                Level.Actions(2, None),
                Level.Actions(None, 2),
            ),
            override=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(Modifier.LevelThree, 3),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(
                Level.Actions(2, Modifier.Control), Level.Actions(2, Modifier.Control)
            ),
            update=KeyEntry(
                Level.Actions(None, None), Level.Actions(3, Modifier.LevelThree)
            ),
            augment=KeyEntry(
                Level.Actions(2, Modifier.Control), Level.Actions(2, Modifier.Control)
            ),
            override=KeyEntry(
                Level.Actions(2, Modifier.Control),
                Level.Actions(3, Modifier.LevelThree),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(
                Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, 2)
            ),
            update=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
            augment=KeyEntry(
                Level.Actions(2, Modifier.Control), Level.Actions(Modifier.Control, 2)
            ),
            override=KeyEntry(Level.Actions(3, None), Level.Actions(None, 3)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None, None), Level.Actions(None, None, None)),
            update=KeyEntry(Level.Actions(None, None, None), Level.Actions(None, None)),
            augment=KeyEntry(
                Level.Actions(None, None), Level.Actions(None, None, None)
            ),
            override=KeyEntry(
                Level.Actions(None, None), Level.Actions(None, None, None)
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None, None), Level.Actions(None, None, None)),
            update=KeyEntry(
                Level.Actions(3, None, Modifier.LevelThree),
                Level.Actions(Modifier.LevelThree, 3),
            ),
            augment=KeyEntry(
                Level.Actions(3, None, Modifier.LevelThree),
                Level.Actions(Modifier.LevelThree, 3),
            ),
            override=KeyEntry(
                Level.Actions(3, None, Modifier.LevelThree),
                Level.Actions(Modifier.LevelThree, 3),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(
                Level.Actions(2, Modifier.Control),
                Level.Actions(Modifier.Control, None, 2),
            ),
            update=KeyEntry(Level.Actions(None, None, None), Level.Actions(None, None)),
            augment=KeyEntry(
                Level.Actions(2, Modifier.Control),
                Level.Actions(Modifier.Control, None, 2),
            ),
            override=KeyEntry(
                Level.Actions(2, Modifier.Control),
                Level.Actions(Modifier.Control, None, 2),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(
                Level.Actions(2, Modifier.Control),
                Level.Actions(Modifier.Control, None, 2),
            ),
            update=KeyEntry(
                Level.Actions(3, None, Modifier.LevelThree),
                Level.Actions(Modifier.LevelThree, 3),
            ),
            augment=KeyEntry(
                Level.Actions(2, Modifier.Control),
                Level.Actions(Modifier.Control, None, 2),
            ),
            override=KeyEntry(
                Level.Actions(3, None, Modifier.LevelThree),
                Level.Actions(Modifier.LevelThree, 3),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Multiple keysyms -> single keysyms"),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(Level.Actions(None), Level.Actions(None)),
            augment=KeyEntry(
                Level.Actions(None, None), Level.Actions(2, Modifier.Control)
            ),
            override=KeyEntry(
                Level.Actions(None, None), Level.Actions(2, Modifier.Control)
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(None, None), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(Level.Actions(3), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(3), Level.Actions(2, Modifier.Control)),
            override=KeyEntry(Level.Actions(3), Level.Actions(3)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            update=KeyEntry(Level.Actions(None), Level.Actions(None)),
            augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            override=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.ActionsOnly),
            KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            update=KeyEntry(Level.Actions(3), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(2, None), Level.Actions(None, 2)),
            override=KeyEntry(Level.Actions(3), Level.Actions(3)),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Mix"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Actions(2)),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            augment=KeyEntry(Level.Actions(2), Level.Actions(3, Modifier.LevelThree)),
            override=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3, Modifier.LevelThree),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Actions(2, Modifier.Control)),
            update=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(2, Modifier.Control), Level.Actions(3)),
            override=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
            augment=KeyEntry(Level.Actions(2), Level.Actions(2, Modifier.Control)),
            override=KeyEntry(Level.Actions(3, Modifier.LevelThree), Level.Actions(3)),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Mix"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Keysyms("a"), Level.Actions(2)),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree), Level.Keysyms("X", "Y")
            ),
            augment=KeyEntry(
                Level.Mix(("a",), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (2,)),
            ),
            override=KeyEntry(
                Level.Mix(("a",), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (2,)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Multiple keysyms/actions –> single"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(Level.Actions(3), Level.Keysyms("X")),
            augment=KeyEntry(
                Level.Mix(("a", "b"), (3,)),
                Level.Mix(("X",), (2, Modifier.Control)),
            ),
            override=KeyEntry(
                Level.Mix(("a", "b"), (3,)),
                Level.Mix(("X",), (2, Modifier.Control)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Multiple keysyms/actions –> multiple (xor)"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree), Level.Keysyms("X", "Y")
            ),
            augment=KeyEntry(
                Level.Mix(("a", "b"), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (2, Modifier.Control)),
            ),
            override=KeyEntry(
                Level.Mix(("a", "b"), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (2, Modifier.Control)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Multiple keysyms/actions –> multiple (mix)"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Keysyms("a", None), Level.Actions(2, None)),
            update=KeyEntry(
                Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
            ),
            augment=KeyEntry(
                Level.Mix(("a", None), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (2, None)),
            ),
            override=KeyEntry(
                Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Keysyms("a", "b"), Level.Actions(2, Modifier.Control)),
            update=KeyEntry(
                Level.Mix(("x", None), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (3, None)),
            ),
            augment=KeyEntry(
                Level.Mix(("a", "b"), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (2, Modifier.Control)),
            ),
            override=KeyEntry(
                Level.Mix(("x", None), (3, Modifier.LevelThree)),
                Level.Mix(("X", "Y"), (3, None)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Multiple (mix) –> multiple keysyms/actions"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(
                Level.Mix(("a", "b"), (2, Modifier.Control)),
                Level.Mix(("A", "B"), (2, Modifier.Control)),
            ),
            update=KeyEntry(Level.Keysyms("x", None), Level.Actions(3, None)),
            augment=KeyEntry(
                Level.Mix(("a", "b"), (2, Modifier.Control)),
                Level.Mix(("A", "B"), (2, Modifier.Control)),
            ),
            override=KeyEntry(
                Level.Mix(("x", None), (2, Modifier.Control)),
                Level.Mix(("A", "B"), (3, None)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(
                Level.Mix(("a", None), (2, Modifier.Control)),
                Level.Mix(("A", "B"), (2, None)),
            ),
            update=KeyEntry(
                Level.Keysyms("x", "y"), Level.Actions(3, Modifier.LevelThree)
            ),
            augment=KeyEntry(
                Level.Mix(("a", None), (2, Modifier.Control)),
                Level.Mix(("A", "B"), (2, None)),
            ),
            override=KeyEntry(
                Level.Mix(("x", "y"), (2, Modifier.Control)),
                Level.Mix(("A", "B"), (3, Modifier.LevelThree)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Multiple (mix) –> multiple (mix)"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(
                Level.Mix(("a", "b"), (2, Modifier.Control)),
                Level.Mix((None, "B"), (2, None)),
            ),
            update=KeyEntry(
                Level.Mix((None, "y"), (3, None)),
                Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
            ),
            augment=KeyEntry(
                Level.Mix(("a", "b"), (2, Modifier.Control)),
                Level.Mix((None, "B"), (2, None)),
            ),
            override=KeyEntry(
                Level.Mix((None, "y"), (3, None)),
                Level.Mix(("X", "Y"), (3, Modifier.LevelThree)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(
                Level.Mix(("a", None), (2, None)),
                Level.Mix((None, "B"), (None, Modifier.Control)),
            ),
            update=KeyEntry(
                Level.Mix((None, "y"), (None, Modifier.LevelThree)),
                Level.Mix(("X", None), (3, None)),
            ),
            augment=KeyEntry(
                Level.Mix(("a", None), (2, None)),
                Level.Mix((None, "B"), (None, Modifier.Control)),
            ),
            override=KeyEntry(
                Level.Mix((None, "y"), (None, Modifier.LevelThree)),
                Level.Mix(("X", None), (3, None)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Mismatch count with mix"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(
                Level.Keysyms("a"),
                Level.Keysyms("A", "B"),
            ),
            update=KeyEntry(
                Level.Actions(3, Modifier.LevelThree),
                Level.Actions(3),
            ),
            augment=KeyEntry(
                Level.Mix(("a"), (3, Modifier.LevelThree)),
                Level.Mix(("A", "B"), (3,)),
            ),
            override=KeyEntry(
                Level.Mix(("a"), (3, Modifier.LevelThree)),
                Level.Mix(("A", "B"), (3,)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(
                Level.Actions(3),
                Level.Actions(3, Modifier.LevelThree),
            ),
            update=KeyEntry(
                Level.Keysyms("A", "B"),
                Level.Keysyms("a"),
            ),
            augment=KeyEntry(
                Level.Mix(("A", "B"), (3,)),
                Level.Mix(("a"), (3, Modifier.LevelThree)),
            ),
            override=KeyEntry(
                Level.Mix(("A", "B"), (3,)),
                Level.Mix(("a"), (3, Modifier.LevelThree)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(
                Level.Mix(("a",), (2,)),
                Level.Mix(("A", "B"), (2, Modifier.Control)),
            ),
            update=KeyEntry(
                Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                Level.Mix(("X",), (3,)),
            ),
            augment=KeyEntry(
                Level.Mix(("a",), (2,)),
                Level.Mix(("A", "B"), (2, Modifier.Control)),
            ),
            override=KeyEntry(
                Level.Mix(("x", "y"), (3, Modifier.LevelThree)),
                Level.Mix(("X",), (3,)),
            ),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Issue #564"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Keysyms("A")),
            update=KeyEntry(Level.Mix(("A", "A"), (3, Modifier.LevelThree))),
            augment=KeyEntry(Level.Mix(("A",), (3, Modifier.LevelThree))),
            override=KeyEntry(Level.Mix(("A", "A"), (3, Modifier.LevelThree))),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Drop NoSymbol/NoAction"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Mix(("A",), (2,))),
            update=KeyEntry(Level.Mix((None, "Y", None), (None, 3, None))),
            augment=KeyEntry(Level.Mix(("A",), (2,))),
            override=KeyEntry(Level.Mix(("Y",), (3,))),
            implementations=Implementation.xkbcommon,
        ),
        Comment("Drop NoSymbol/NoAction and invalid keysyms"),
        TestEntry(
            TestId(TestType.KeysymsAndActions),
            KeyEntry(Level.Mix(("notAKeysym", None, "thisEither"), (None, None))),
            update=KeyEntry(Level.Mix((None, None), (None, None))),
            augment=KeyEntry(Level.Keysyms(None)),
            override=KeyEntry(Level.Keysyms(None)),
            replace=KeyEntry(Level.Keysyms(None)),
            implementations=Implementation.xkbcommon,
        ),
    ),
).add_keysyms()


################################################################################
#
# Keycodes tests
#
################################################################################

KEYCODES_TESTS_BASE = ComponentTestTemplate(
    "Keycodes",
    component=Component.keycodes,
    base_template=ComponentTemplate(
        """
        ${mode}<1> = 241;
        ${mode}<2> = 242;

        ${mode}alias <A> = <1>;
        ${mode}alias <B> = <2>;

        ${mode}indicator 1 = "Caps Lock";
        ${mode}indicator 2 = "Num Lock";
        ${mode}indicator 3 = "Scroll Lock";
        ${mode}indicator 4 = "Compose";
        ${mode}indicator 5 = "Kana";
        """
    ),
    update_template=ComponentTemplate(
        """
        ${mode}<1> = 241;            // Unchanged
        ${mode}<2> = 244;            // Changed
        ${mode}<3> = 243;            // New

        ${mode}alias <A> = <1>;    // Unchanged
        ${mode}alias <B> = <3>;    // Changed
        ${mode}alias <C> = <3>;    // New

        ${mode}indicator 1 = "Caps Lock";   // Unchanged
        ${mode}indicator 6 = "Num Lock";    // Changed index (free)
        ${mode}indicator 5 = "Scroll Lock"; // Changed index (not free)
        ${mode}indicator 4 = "XXXX";        // Changed name
        ${mode}indicator 7 = "Suspend";     // New
        """
    ),
    augment="""
        <1> = 241;
        <2> = 242;
        <3> = 243;
        alias <A> = <1>;
        alias <B> = <2>;
        alias <C> = <3>;
        indicator 1 = "Caps Lock";
        indicator 2 = "Num Lock";
        indicator 3 = "Scroll Lock";
        indicator 4 = "Compose";
        indicator 5 = "Kana";
        indicator 7 = "Suspend";
        """,
    override="""
        <1> = 241;
        <2> = 244;
        <3> = 243;
        alias <A> = <1>;
        alias <B> = <3>;
        alias <C> = <3>;
        indicator 1 = "Caps Lock";
        indicator 4 = "XXXX";
        indicator 5 = "Scroll Lock";
        indicator 6 = "Num Lock";
        indicator 7 = "Suspend";
        """,
)


################################################################################
#
# Main
#
################################################################################

if __name__ == "__main__":
    # Parse commands
    parser = argparse.ArgumentParser(description="Generate merge mode tests")
    parser.add_argument(
        "compiler",
        type=XkbCompiler.parse,
        nargs="?",
        default="xkbcommon",
        help="XKB compiler to use",
    )
    parser.add_argument(
        "--root",
        type=Path,
        default=ROOT,
        help="Path to the root of the project (default: %(default)s)",
    )
    parser.add_argument(
        "--c-out",
        type=Path,
        # default=ROOT / TestGroup.c_template.stem,
        help="Path to the output C test file (default: %(default)s)",
    )
    parser.add_argument("--no-xkb", action="store_true", help="Do not update XKB data")
    parser.add_argument("--debug", action="store_true", help="Activate debug mode")
    args = parser.parse_args()

    # Prepare data
    template_loader = jinja2.FileSystemLoader(args.root, encoding="utf-8")
    jinja_env = jinja2.Environment(
        loader=template_loader,
        keep_trailing_newline=True,
        trim_blocks=True,
        lstrip_blocks=True,
    )

    def escape_quotes(s: str) -> str:
        return s.replace('"', '\\"')

    jinja_env.filters["escape_quotes"] = escape_quotes
    compiler: XkbCompiler = args.compiler

    SYMBOLS_TESTS = SYMBOLS_TESTS_BOTH.with_implementation(
        Implementation.xkbcommon if compiler.extended_syntax else Implementation.x11
    )

    KEYCODES_TESTS = SYMBOLS_TESTS.get_keycodes(KEYCODES_TESTS_BASE)

    TESTS = (
        KeymapTestTemplate(
            title="",
            keycodes=KEYCODES_TESTS,
            types=TYPES_TESTS,
            compat=COMPAT_TESTS,
            symbols=SYMBOLS_TESTS.get_symbols(debug=args.debug),
        ),
    )

    # Write tests
    TESTS[0].write(
        root=args.root,
        jinja_env=jinja_env,
        c_file=args.c_out,
        xkb=not args.no_xkb,
        compiler=compiler,
        tests=TESTS,
    )
