From cc614cfed2ecd138a696f32a353881823ce0281e Mon Sep 17 00:00:00 2001
From: MikaKerman <mika.kerman@gmail.com>
Date: Thu, 23 Jan 2025 17:25:12 +0200
Subject: [PATCH] Add adaptive cards message formatting and unit tests

- Introduced new `adaptive_cards.py` module for formatting messages as adaptive cards, including functions for various block types such as headers, lines, facts, and expandable sections.
- Implemented utility functions to format message blocks and handle different styles and colors.
- Added comprehensive unit tests for adaptive card formatting, covering various scenarios including headers, icons, text styles, and expandable blocks.
- Created fixture files for expected JSON outputs to validate the formatting logic.
---
 elementary/messages/formats/__init__.py       |   0
 elementary/messages/formats/adaptive_cards.py | 229 +++++++++++
 tests/unit/messages/__init__.py               |   0
 tests/unit/messages/formats/__init__.py       |   0
 .../formats/adaptive_cards/__init__.py        |   0
 .../fixtures/all_blocks_green.json            |  93 +++++
 .../fixtures/all_blocks_no_color.json         |  93 +++++
 .../fixtures/all_blocks_red.json              |  93 +++++
 .../fixtures/all_blocks_yellow.json           |  93 +++++
 .../adaptive_cards/fixtures/all_icons.json    |  17 +
 .../adaptive_cards/fixtures/bullet_list.json  |  32 ++
 .../fixtures/code_block_200.json              |  28 ++
 .../fixtures/code_block_50.json               |  28 ++
 .../fixtures/code_block_500.json              |  28 ++
 .../fixtures/colored_header.json              |  25 ++
 .../fixtures/divider_blocks.json              |  44 +++
 .../fixtures/expandable_block.json            |  42 ++
 .../adaptive_cards/fixtures/fact_list.json    |  21 +
 .../fixtures/nested_expandable.json           |  77 ++++
 .../fixtures/simple_header.json               |  25 ++
 .../fixtures/text_length_1000.json            |  17 +
 .../fixtures/text_length_200.json             |  17 +
 .../fixtures/text_length_50.json              |  17 +
 .../fixtures/text_length_500.json             |  17 +
 .../adaptive_cards/fixtures/text_styles.json  |  17 +
 .../adaptive_cards/test_adaptive_cards.py     | 373 ++++++++++++++++++
 tests/unit/messages/utils.py                  |  28 ++
 27 files changed, 1454 insertions(+)
 create mode 100644 elementary/messages/formats/__init__.py
 create mode 100644 elementary/messages/formats/adaptive_cards.py
 create mode 100644 tests/unit/messages/__init__.py
 create mode 100644 tests/unit/messages/formats/__init__.py
 create mode 100644 tests/unit/messages/formats/adaptive_cards/__init__.py
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_green.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_no_color.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_red.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_yellow.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/all_icons.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/bullet_list.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/code_block_200.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/code_block_50.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/code_block_500.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/colored_header.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/divider_blocks.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/expandable_block.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/fact_list.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/nested_expandable.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/simple_header.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/text_length_1000.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/text_length_200.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/text_length_50.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/text_length_500.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/fixtures/text_styles.json
 create mode 100644 tests/unit/messages/formats/adaptive_cards/test_adaptive_cards.py
 create mode 100644 tests/unit/messages/utils.py

diff --git a/elementary/messages/formats/__init__.py b/elementary/messages/formats/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/elementary/messages/formats/adaptive_cards.py b/elementary/messages/formats/adaptive_cards.py
new file mode 100644
index 000000000..a526e1184
--- /dev/null
+++ b/elementary/messages/formats/adaptive_cards.py
@@ -0,0 +1,229 @@
+import uuid
+from typing import Any, Dict, List, Optional
+
+from elementary.messages.blocks import (
+    CodeBlock,
+    DividerBlock,
+    ExpandableBlock,
+    FactListBlock,
+    HeaderBlock,
+    Icon,
+    IconBlock,
+    InlineBlock,
+    LineBlock,
+    LinesBlock,
+    LinkBlock,
+    TextBlock,
+    TextStyle,
+)
+from elementary.messages.message_body import Color, MessageBlock, MessageBody
+
+ICON_TO_HTML = {
+    Icon.RED_TRIANGLE: "đŸ”ē",
+    Icon.X: "❌",
+    Icon.WARNING: "⚠ī¸",
+    Icon.EXCLAMATION: "❗",
+    Icon.CHECK: "✅",
+    Icon.MAGNIFYING_GLASS: "🔎",
+    Icon.HAMMER_AND_WRENCH: "🛠ī¸",
+    Icon.POLICE_LIGHT: "🚨",
+    Icon.INFO: "ℹī¸",
+    Icon.EYE: "👁ī¸",
+    Icon.GEAR: "⚙ī¸",
+    Icon.BELL: "🔔",
+}
+
+COLOR_TO_STYLE = {
+    Color.RED: "Attention",
+    Color.YELLOW: "Warning",
+    Color.GREEN: "Good",
+}
+
+
+def format_icon(icon: Icon) -> str:
+    return ICON_TO_HTML[icon]
+
+
+def format_text_block(block: TextBlock) -> str:
+    if block.style == TextStyle.BOLD:
+        return f"**{block.text}**"
+    elif block.style == TextStyle.ITALIC:
+        return f"_{block.text}_"
+    else:
+        return block.text
+
+
+def format_inline_block(block: InlineBlock) -> str:
+    if isinstance(block, IconBlock):
+        return format_icon(block.icon)
+    elif isinstance(block, TextBlock):
+        return format_text_block(block)
+    elif isinstance(block, LinkBlock):
+        return f"[{block.text}]({block.url})"
+    else:
+        raise ValueError(f"Unsupported inline block type: {type(block)}")
+
+
+def format_line_block_text(block: LineBlock) -> str:
+    return block.sep.join([format_inline_block(inline) for inline in block.inlines])
+
+
+def format_line_block(block: LineBlock) -> Dict[str, Any]:
+    text = format_line_block_text(block)
+
+    return {
+        "type": "TextBlock",
+        "text": text,
+        "wrap": True,
+    }
+
+
+def format_lines_block(block: LinesBlock) -> List[Dict[str, Any]]:
+    return [format_line_block(line_block) for line_block in block.lines]
+
+
+def format_header_block(
+    block: HeaderBlock, color: Optional[Color] = None
+) -> Dict[str, Any]:
+    return {
+        "type": "Container",
+        "items": [
+            {
+                "type": "TextBlock",
+                "text": block.text,
+                "weight": "bolder",
+                "size": "large",
+                "wrap": True,
+            }
+        ],
+        "style": COLOR_TO_STYLE[color] if color else "Default",
+    }
+
+
+def format_code_block(block: CodeBlock) -> Dict[str, Any]:
+    return {
+        "type": "Container",
+        "style": "emphasis",
+        "items": [
+            {
+                "type": "RichTextBlock",
+                "inlines": [
+                    {
+                        "type": "TextRun",
+                        "text": block.text,
+                        "fontType": "Monospace",
+                    }
+                ],
+            }
+        ],
+    }
+
+
+def format_fact_list_block(block: FactListBlock) -> Dict[str, Any]:
+    return {
+        "type": "FactSet",
+        "facts": [
+            {
+                "title": format_line_block_text(fact.title),
+                "value": format_line_block_text(fact.value),
+            }
+            for fact in block.facts
+        ],
+    }
+
+
+def format_message_block(
+    block: MessageBlock, color: Optional[Color] = None
+) -> List[Dict[str, Any]]:
+    if isinstance(block, HeaderBlock):
+        return [format_header_block(block, color)]
+    elif isinstance(block, CodeBlock):
+        return [format_code_block(block)]
+    elif isinstance(block, LinesBlock):
+        return format_lines_block(block)
+    elif isinstance(block, FactListBlock):
+        return [format_fact_list_block(block)]
+    elif isinstance(block, ExpandableBlock):
+        return format_expandable_block(block)
+    else:
+        raise ValueError(f"Unsupported message block type: {type(block)}")
+
+
+def split_message_blocks_by_divider(
+    blocks: List[MessageBlock],
+) -> List[List[MessageBlock]]:
+    first_divider_index = next(
+        (i for i, block in enumerate(blocks) if isinstance(block, DividerBlock)),
+        None,
+    )
+    if first_divider_index is None:
+        return [blocks] if blocks else []
+    return [
+        blocks[:first_divider_index],
+        *split_message_blocks_by_divider(blocks[first_divider_index + 1 :]),
+    ]
+
+
+def format_divided_message_blocks(
+    blocks: List[MessageBlock],
+    divider: bool = False,
+    color: Optional[Color] = None,
+) -> Dict[str, Any]:
+    return {
+        "type": "Container",
+        "separator": divider,
+        "items": [
+            item for block in blocks for item in format_message_block(block, color)
+        ],
+    }
+
+
+def format_expandable_block(block: ExpandableBlock) -> List[Dict[str, Any]]:
+    block_title = block.title
+    expandable_target_id = f"expandable-{uuid.uuid4()}"
+    return [
+        {
+            "type": "ActionSet",
+            "actions": [
+                {
+                    "type": "Action.ToggleVisibility",
+                    "title": block_title,
+                    "targetElements": [expandable_target_id],
+                }
+            ],
+        },
+        {
+            "type": "Container",
+            "id": expandable_target_id,
+            "items": format_message_blocks(block.body),
+            "isVisible": block.expanded,
+        },
+    ]
+
+
+def format_message_blocks(
+    blocks: List[MessageBlock], color: Optional[Color] = None
+) -> List[Dict[str, Any]]:
+    if not blocks:
+        return []
+
+    message_blocks = split_message_blocks_by_divider(blocks)
+    # The divider is not a block in adaptive cards, it's a property of the container.
+    return [
+        format_divided_message_blocks(blocks, divider=True, color=color)
+        for blocks in message_blocks
+    ]
+
+
+def format_adaptive_card_body(message: MessageBody) -> List[Dict[str, Any]]:
+    return format_message_blocks(message.blocks, message.color)
+
+
+def format_adaptive_card(message: MessageBody, version: str = "1.6") -> Dict[str, Any]:
+    if version < "1.2" or version > "1.6":
+        raise ValueError(f"Version {version} is not supported")
+    return {
+        "type": "AdaptiveCard",
+        "body": format_adaptive_card_body(message),
+        "version": version,
+    }
diff --git a/tests/unit/messages/__init__.py b/tests/unit/messages/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/unit/messages/formats/__init__.py b/tests/unit/messages/formats/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/unit/messages/formats/adaptive_cards/__init__.py b/tests/unit/messages/formats/adaptive_cards/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_green.json b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_green.json
new file mode 100644
index 000000000..7b2ca00ca
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_green.json
@@ -0,0 +1,93 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "Main Header",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Good"
+        },
+        {
+          "type": "TextBlock",
+          "text": "Normal text **Bold text** _Italic text_",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- First bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- Second bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "\u2705 Check item",
+          "wrap": true
+        },
+        {
+          "type": "FactSet",
+          "facts": [
+            {
+              "title": "Status",
+              "value": "Passed"
+            },
+            {
+              "title": "Tags",
+              "value": "test, example"
+            }
+          ]
+        },
+        {
+          "type": "ActionSet",
+          "actions": [
+            {
+              "type": "Action.ToggleVisibility",
+              "title": "Show Details",
+              "targetElements": [
+                "expandable-00000000-0000-0000-0000-000000000001"
+              ]
+            }
+          ]
+        },
+        {
+          "type": "Container",
+          "id": "expandable-00000000-0000-0000-0000-000000000001",
+          "items": [
+            {
+              "type": "Container",
+              "separator": true,
+              "items": [
+                {
+                  "type": "TextBlock",
+                  "text": "\ud83d\udd0e **Details Section**",
+                  "wrap": true
+                },
+                {
+                  "type": "TextBlock",
+                  "text": "Here's some content with a [link](https://example.com)",
+                  "wrap": true
+                }
+              ]
+            }
+          ],
+          "isVisible": false
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_no_color.json b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_no_color.json
new file mode 100644
index 000000000..3a80ccfac
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_no_color.json
@@ -0,0 +1,93 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "Main Header",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Default"
+        },
+        {
+          "type": "TextBlock",
+          "text": "Normal text **Bold text** _Italic text_",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- First bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- Second bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "\u2705 Check item",
+          "wrap": true
+        },
+        {
+          "type": "FactSet",
+          "facts": [
+            {
+              "title": "Status",
+              "value": "Passed"
+            },
+            {
+              "title": "Tags",
+              "value": "test, example"
+            }
+          ]
+        },
+        {
+          "type": "ActionSet",
+          "actions": [
+            {
+              "type": "Action.ToggleVisibility",
+              "title": "Show Details",
+              "targetElements": [
+                "expandable-00000000-0000-0000-0000-000000000001"
+              ]
+            }
+          ]
+        },
+        {
+          "type": "Container",
+          "id": "expandable-00000000-0000-0000-0000-000000000001",
+          "items": [
+            {
+              "type": "Container",
+              "separator": true,
+              "items": [
+                {
+                  "type": "TextBlock",
+                  "text": "\ud83d\udd0e **Details Section**",
+                  "wrap": true
+                },
+                {
+                  "type": "TextBlock",
+                  "text": "Here's some content with a [link](https://example.com)",
+                  "wrap": true
+                }
+              ]
+            }
+          ],
+          "isVisible": false
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_red.json b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_red.json
new file mode 100644
index 000000000..1029ea203
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_red.json
@@ -0,0 +1,93 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "Main Header",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Attention"
+        },
+        {
+          "type": "TextBlock",
+          "text": "Normal text **Bold text** _Italic text_",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- First bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- Second bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "\u2705 Check item",
+          "wrap": true
+        },
+        {
+          "type": "FactSet",
+          "facts": [
+            {
+              "title": "Status",
+              "value": "Passed"
+            },
+            {
+              "title": "Tags",
+              "value": "test, example"
+            }
+          ]
+        },
+        {
+          "type": "ActionSet",
+          "actions": [
+            {
+              "type": "Action.ToggleVisibility",
+              "title": "Show Details",
+              "targetElements": [
+                "expandable-00000000-0000-0000-0000-000000000001"
+              ]
+            }
+          ]
+        },
+        {
+          "type": "Container",
+          "id": "expandable-00000000-0000-0000-0000-000000000001",
+          "items": [
+            {
+              "type": "Container",
+              "separator": true,
+              "items": [
+                {
+                  "type": "TextBlock",
+                  "text": "\ud83d\udd0e **Details Section**",
+                  "wrap": true
+                },
+                {
+                  "type": "TextBlock",
+                  "text": "Here's some content with a [link](https://example.com)",
+                  "wrap": true
+                }
+              ]
+            }
+          ],
+          "isVisible": false
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_yellow.json b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_yellow.json
new file mode 100644
index 000000000..f379172ba
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_yellow.json
@@ -0,0 +1,93 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "Main Header",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Warning"
+        },
+        {
+          "type": "TextBlock",
+          "text": "Normal text **Bold text** _Italic text_",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- First bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- Second bullet point",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "\u2705 Check item",
+          "wrap": true
+        },
+        {
+          "type": "FactSet",
+          "facts": [
+            {
+              "title": "Status",
+              "value": "Passed"
+            },
+            {
+              "title": "Tags",
+              "value": "test, example"
+            }
+          ]
+        },
+        {
+          "type": "ActionSet",
+          "actions": [
+            {
+              "type": "Action.ToggleVisibility",
+              "title": "Show Details",
+              "targetElements": [
+                "expandable-00000000-0000-0000-0000-000000000001"
+              ]
+            }
+          ]
+        },
+        {
+          "type": "Container",
+          "id": "expandable-00000000-0000-0000-0000-000000000001",
+          "items": [
+            {
+              "type": "Container",
+              "separator": true,
+              "items": [
+                {
+                  "type": "TextBlock",
+                  "text": "\ud83d\udd0e **Details Section**",
+                  "wrap": true
+                },
+                {
+                  "type": "TextBlock",
+                  "text": "Here's some content with a [link](https://example.com)",
+                  "wrap": true
+                }
+              ]
+            }
+          ],
+          "isVisible": false
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/all_icons.json b/tests/unit/messages/formats/adaptive_cards/fixtures/all_icons.json
new file mode 100644
index 000000000..cf93cf6e7
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/all_icons.json
@@ -0,0 +1,17 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "RED_TRIANGLE \ud83d\udd3a X \u274c WARNING \u26a0\ufe0f EXCLAMATION \u2757 CHECK \u2705 MAGNIFYING_GLASS \ud83d\udd0e HAMMER_AND_WRENCH \ud83d\udee0\ufe0f POLICE_LIGHT \ud83d\udea8 INFO \u2139\ufe0f EYE \ud83d\udc41\ufe0f GEAR \u2699\ufe0f BELL \ud83d\udd14",
+          "wrap": true
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/bullet_list.json b/tests/unit/messages/formats/adaptive_cards/fixtures/bullet_list.json
new file mode 100644
index 000000000..23b4ede9d
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/bullet_list.json
@@ -0,0 +1,32 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "- First bullet",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "- Second bullet",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "\u2705 Check item 1",
+          "wrap": true
+        },
+        {
+          "type": "TextBlock",
+          "text": "\u2705 Check item 2",
+          "wrap": true
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_200.json b/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_200.json
new file mode 100644
index 000000000..832e60036
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_200.json
@@ -0,0 +1,28 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "style": "emphasis",
+          "items": [
+            {
+              "type": "RichTextBlock",
+              "inlines": [
+                {
+                  "type": "TextRun",
+                  "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, c",
+                  "fontType": "Monospace"
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_50.json b/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_50.json
new file mode 100644
index 000000000..20e2e7a01
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_50.json
@@ -0,0 +1,28 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "style": "emphasis",
+          "items": [
+            {
+              "type": "RichTextBlock",
+              "inlines": [
+                {
+                  "type": "TextRun",
+                  "text": "Lorem ipsum dolor sit amet, consectetur adipiscing",
+                  "fontType": "Monospace"
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_500.json b/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_500.json
new file mode 100644
index 000000000..9651f8f87
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/code_block_500.json
@@ -0,0 +1,28 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "style": "emphasis",
+          "items": [
+            {
+              "type": "RichTextBlock",
+              "inlines": [
+                {
+                  "type": "TextRun",
+                  "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adip",
+                  "fontType": "Monospace"
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/colored_header.json b/tests/unit/messages/formats/adaptive_cards/fixtures/colored_header.json
new file mode 100644
index 000000000..5738a709d
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/colored_header.json
@@ -0,0 +1,25 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "Test Header",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Good"
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/divider_blocks.json b/tests/unit/messages/formats/adaptive_cards/fixtures/divider_blocks.json
new file mode 100644
index 000000000..97616dc4c
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/divider_blocks.json
@@ -0,0 +1,44 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "First Section",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Default"
+        }
+      ]
+    },
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "Second Section",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Default"
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/expandable_block.json b/tests/unit/messages/formats/adaptive_cards/fixtures/expandable_block.json
new file mode 100644
index 000000000..36d6ebd6c
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/expandable_block.json
@@ -0,0 +1,42 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "ActionSet",
+          "actions": [
+            {
+              "type": "Action.ToggleVisibility",
+              "title": "Show More",
+              "targetElements": [
+                "expandable-00000000-0000-0000-0000-000000000001"
+              ]
+            }
+          ]
+        },
+        {
+          "type": "Container",
+          "id": "expandable-00000000-0000-0000-0000-000000000001",
+          "items": [
+            {
+              "type": "Container",
+              "separator": true,
+              "items": [
+                {
+                  "type": "TextBlock",
+                  "text": "Hidden content",
+                  "wrap": true
+                }
+              ]
+            }
+          ],
+          "isVisible": false
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/fact_list.json b/tests/unit/messages/formats/adaptive_cards/fixtures/fact_list.json
new file mode 100644
index 000000000..64942a9ae
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/fact_list.json
@@ -0,0 +1,21 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "FactSet",
+          "facts": [
+            {
+              "title": "Status",
+              "value": "Passed"
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/nested_expandable.json b/tests/unit/messages/formats/adaptive_cards/fixtures/nested_expandable.json
new file mode 100644
index 000000000..c53b6e844
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/nested_expandable.json
@@ -0,0 +1,77 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "ActionSet",
+          "actions": [
+            {
+              "type": "Action.ToggleVisibility",
+              "title": "Outer Block",
+              "targetElements": [
+                "expandable-00000000-0000-0000-0000-000000000001"
+              ]
+            }
+          ]
+        },
+        {
+          "type": "Container",
+          "id": "expandable-00000000-0000-0000-0000-000000000001",
+          "items": [
+            {
+              "type": "Container",
+              "separator": true,
+              "items": [
+                {
+                  "type": "TextBlock",
+                  "text": "\ud83d\udd0e **Title with Icon**",
+                  "wrap": true
+                },
+                {
+                  "type": "TextBlock",
+                  "text": "Some content with a [link](https://example.com)",
+                  "wrap": true
+                },
+                {
+                  "type": "ActionSet",
+                  "actions": [
+                    {
+                      "type": "Action.ToggleVisibility",
+                      "title": "Inner Block",
+                      "targetElements": [
+                        "expandable-00000000-0000-0000-0000-000000000002"
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "type": "Container",
+                  "id": "expandable-00000000-0000-0000-0000-000000000002",
+                  "items": [
+                    {
+                      "type": "Container",
+                      "separator": true,
+                      "items": [
+                        {
+                          "type": "TextBlock",
+                          "text": "Inner content",
+                          "wrap": true
+                        }
+                      ]
+                    }
+                  ],
+                  "isVisible": true
+                }
+              ]
+            }
+          ],
+          "isVisible": false
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/simple_header.json b/tests/unit/messages/formats/adaptive_cards/fixtures/simple_header.json
new file mode 100644
index 000000000..f56f090bb
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/simple_header.json
@@ -0,0 +1,25 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "Container",
+          "items": [
+            {
+              "type": "TextBlock",
+              "text": "Test Header",
+              "weight": "bolder",
+              "size": "large",
+              "wrap": true
+            }
+          ],
+          "style": "Default"
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_1000.json b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_1000.json
new file mode 100644
index 000000000..f7a44c0fa
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_1000.json
@@ -0,0 +1,17 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, con",
+          "wrap": true
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_200.json b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_200.json
new file mode 100644
index 000000000..a488e897e
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_200.json
@@ -0,0 +1,17 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, c",
+          "wrap": true
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_50.json b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_50.json
new file mode 100644
index 000000000..9d43c2072
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_50.json
@@ -0,0 +1,17 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "Lorem ipsum dolor sit amet, consectetur adipiscing",
+          "wrap": true
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_500.json b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_500.json
new file mode 100644
index 000000000..7eee3618b
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/text_length_500.json
@@ -0,0 +1,17 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adip",
+          "wrap": true
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/fixtures/text_styles.json b/tests/unit/messages/formats/adaptive_cards/fixtures/text_styles.json
new file mode 100644
index 000000000..01643cc8c
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/fixtures/text_styles.json
@@ -0,0 +1,17 @@
+{
+  "type": "AdaptiveCard",
+  "body": [
+    {
+      "type": "Container",
+      "separator": true,
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "Normal text **Bold text** _Italic text_",
+          "wrap": true
+        }
+      ]
+    }
+  ],
+  "version": "1.6"
+}
diff --git a/tests/unit/messages/formats/adaptive_cards/test_adaptive_cards.py b/tests/unit/messages/formats/adaptive_cards/test_adaptive_cards.py
new file mode 100644
index 000000000..d4faca6ae
--- /dev/null
+++ b/tests/unit/messages/formats/adaptive_cards/test_adaptive_cards.py
@@ -0,0 +1,373 @@
+"""
+Tests for adaptive cards message format.
+
+The expected results are saved as JSON files in the resources/ directory.
+These files can be viewed and tested using:
+1. VS Code's Adaptive Cards extension
+2. Adaptive Cards Designer (https://adaptivecards.io/designer/) - useful for checking how cards
+   look across different products, devices, themes and schema versions
+"""
+
+import uuid
+from pathlib import Path
+from typing import List, Union
+
+import pytest
+
+from elementary.messages.block_builders import BulletListBlock
+from elementary.messages.blocks import (
+    CodeBlock,
+    DividerBlock,
+    ExpandableBlock,
+    FactBlock,
+    FactListBlock,
+    HeaderBlock,
+    Icon,
+    IconBlock,
+    LineBlock,
+    LinesBlock,
+    LinkBlock,
+    TextBlock,
+    TextStyle,
+)
+from elementary.messages.formats.adaptive_cards import format_adaptive_card
+from elementary.messages.message_body import Color, MessageBody
+from tests.unit.messages.utils import assert_expected_json, get_expected_json_path
+
+FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures"
+
+
+@pytest.fixture(autouse=True)
+def mock_uuid(monkeypatch):
+    class MockUUID:
+        def __init__(self):
+            self.counter = 0
+
+        def __call__(self):
+            self.counter += 1
+            return uuid.UUID(
+                f"00000000-0000-0000-0000-{self.counter:012d}"  # noqa: E231
+            )
+
+    mock = MockUUID()
+    monkeypatch.setattr(uuid, "uuid4", mock)
+    return mock
+
+
+def test_format_message_body_simple_header():
+    message_body = MessageBody(blocks=[HeaderBlock(text="Test Header")], color=None)
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "simple_header.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_colored_header():
+    message_body = MessageBody(
+        blocks=[HeaderBlock(text="Test Header")], color=Color.GREEN
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "colored_header.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_all_icons():
+    icon_blocks: List[Union[TextBlock, IconBlock]] = []
+    for icon in Icon:
+        icon_blocks.append(TextBlock(text=icon.name))
+        icon_blocks.append(IconBlock(icon=icon))
+    message_body = MessageBody(
+        blocks=[LinesBlock(lines=[LineBlock(inlines=icon_blocks)])]
+    )
+    result = format_adaptive_card(message_body)
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "all_icons.json")
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_text_styles():
+    message_body = MessageBody(
+        blocks=[
+            LinesBlock(
+                lines=[
+                    LineBlock(
+                        inlines=[
+                            TextBlock(text="Normal text"),
+                            TextBlock(text="Bold text", style=TextStyle.BOLD),
+                            TextBlock(text="Italic text", style=TextStyle.ITALIC),
+                        ]
+                    )
+                ]
+            )
+        ]
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "text_styles.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_fact_list():
+    message_body = MessageBody(
+        blocks=[
+            FactListBlock(
+                facts=[
+                    FactBlock(
+                        title=LineBlock(inlines=[TextBlock(text="Status")]),
+                        value=LineBlock(inlines=[TextBlock(text="Passed")]),
+                    ),
+                ]
+            )
+        ]
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "fact_list.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_expandable_block():
+    message_body = MessageBody(
+        blocks=[
+            ExpandableBlock(
+                title="Show More",
+                body=[
+                    LinesBlock(
+                        lines=[LineBlock(inlines=[TextBlock(text="Hidden content")])]
+                    )
+                ],
+                expanded=False,
+            )
+        ]
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "expandable_block.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_divider_blocks():
+    message_body = MessageBody(
+        blocks=[
+            HeaderBlock(text="First Section"),
+            DividerBlock(),
+            HeaderBlock(text="Second Section"),
+        ]
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "divider_blocks.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_bullet_lists():
+    message_body = MessageBody(
+        blocks=[
+            BulletListBlock(
+                icon="-",
+                lines=[
+                    LineBlock(inlines=[TextBlock(text="First bullet")]),
+                    LineBlock(inlines=[TextBlock(text="Second bullet")]),
+                ],
+            ),
+            BulletListBlock(
+                icon=Icon.CHECK,
+                lines=[
+                    LineBlock(inlines=[TextBlock(text="Check item 1")]),
+                    LineBlock(inlines=[TextBlock(text="Check item 2")]),
+                ],
+            ),
+        ]
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "bullet_list.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+def test_format_message_body_nested_expandable():
+    message_body = MessageBody(
+        blocks=[
+            ExpandableBlock(
+                title="Outer Block",
+                body=[
+                    LinesBlock(
+                        lines=[
+                            LineBlock(
+                                inlines=[
+                                    IconBlock(icon=Icon.MAGNIFYING_GLASS),
+                                    TextBlock(
+                                        text="Title with Icon", style=TextStyle.BOLD
+                                    ),
+                                ]
+                            ),
+                            LineBlock(
+                                inlines=[
+                                    TextBlock(text="Some content with a"),
+                                    LinkBlock(text="link", url="https://example.com"),
+                                ]
+                            ),
+                        ]
+                    ),
+                    ExpandableBlock(
+                        title="Inner Block",
+                        body=[
+                            LinesBlock(
+                                lines=[
+                                    LineBlock(inlines=[TextBlock(text="Inner content")])
+                                ]
+                            )
+                        ],
+                        expanded=True,
+                    ),
+                ],
+                expanded=False,
+            )
+        ]
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, "nested_expandable.json")
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+@pytest.mark.parametrize(
+    "color,expected_file",
+    [
+        pytest.param(None, "all_blocks_no_color.json", id="no_color"),
+        pytest.param(Color.RED, "all_blocks_red.json", id="red"),
+        pytest.param(Color.YELLOW, "all_blocks_yellow.json", id="yellow"),
+        pytest.param(Color.GREEN, "all_blocks_green.json", id="green"),
+    ],
+)
+def test_format_message_body_all_blocks(color, expected_file):
+    """Test a comprehensive message that includes all block types with different colors."""
+    message_body = MessageBody(
+        blocks=[
+            HeaderBlock(text="Main Header"),
+            LinesBlock(
+                lines=[
+                    LineBlock(
+                        inlines=[
+                            TextBlock(text="Normal text"),
+                            TextBlock(text="Bold text", style=TextStyle.BOLD),
+                            TextBlock(text="Italic text", style=TextStyle.ITALIC),
+                        ]
+                    )
+                ]
+            ),
+            BulletListBlock(
+                icon="-",
+                lines=[
+                    LineBlock(inlines=[TextBlock(text="First bullet point")]),
+                    LineBlock(inlines=[TextBlock(text="Second bullet point")]),
+                ],
+            ),
+            BulletListBlock(
+                icon=Icon.CHECK,
+                lines=[LineBlock(inlines=[TextBlock(text="Check item")])],
+            ),
+            FactListBlock(
+                facts=[
+                    FactBlock(
+                        title=LineBlock(inlines=[TextBlock(text="Status")]),
+                        value=LineBlock(inlines=[TextBlock(text="Passed")]),
+                    ),
+                    FactBlock(
+                        title=LineBlock(inlines=[TextBlock(text="Tags")]),
+                        value=LineBlock(inlines=[TextBlock(text="test, example")]),
+                    ),
+                ]
+            ),
+            ExpandableBlock(
+                title="Show Details",
+                body=[
+                    LinesBlock(
+                        lines=[
+                            LineBlock(
+                                inlines=[
+                                    IconBlock(icon=Icon.MAGNIFYING_GLASS),
+                                    TextBlock(
+                                        text="Details Section", style=TextStyle.BOLD
+                                    ),
+                                ]
+                            ),
+                            LineBlock(
+                                inlines=[
+                                    TextBlock(text="Here's some content with a"),
+                                    LinkBlock(text="link", url="https://example.com"),
+                                ]
+                            ),
+                        ]
+                    )
+                ],
+                expanded=False,
+            ),
+        ],
+        color=color,
+    )
+    expected_json_path = get_expected_json_path(FIXTURES_DIR, expected_file)
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+@pytest.mark.parametrize(
+    "version,should_raise",
+    [
+        pytest.param("1.6", False, id="supported_version"),
+        pytest.param("1.1", True, id="unsupported_version_low"),
+        pytest.param("1.7", True, id="unsupported_version_high"),
+    ],
+)
+def test_format_version_validation(version, should_raise):
+    message_body = MessageBody(blocks=[HeaderBlock(text="Test")])
+
+    if should_raise:
+        try:
+            format_adaptive_card(message_body, version=version)
+            assert False, f"Expected ValueError for version {version}"
+        except ValueError:
+            pass
+    else:
+        result = format_adaptive_card(message_body, version=version)
+        assert result["version"] == version
+        assert result["type"] == "AdaptiveCard"
+
+
+@pytest.mark.parametrize(
+    "text_length",
+    [
+        pytest.param(50, id="short_code"),
+        pytest.param(200, id="medium_code"),
+        pytest.param(500, id="long_code"),
+    ],
+)
+def test_format_message_body_code_block(text_length: int):
+    lorem_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * (
+        (text_length + 49) // 50
+    )
+    lorem_text = lorem_text[:text_length]
+
+    message_body = MessageBody(blocks=[CodeBlock(text=lorem_text)])
+    expected_json_path = get_expected_json_path(
+        FIXTURES_DIR, f"code_block_{text_length}.json"
+    )
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
+
+
+@pytest.mark.parametrize(
+    "text_length",
+    [
+        pytest.param(50, id="short_text"),
+        pytest.param(200, id="medium_text"),
+        pytest.param(500, id="long_text"),
+        pytest.param(1000, id="very_long_text"),
+    ],
+)
+def test_format_message_body_text_length(text_length: int):
+    lorem_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * (
+        (text_length + 49) // 50
+    )
+    lorem_text = lorem_text[:text_length]
+
+    message_body = MessageBody(
+        blocks=[LinesBlock(lines=[LineBlock(inlines=[TextBlock(text=lorem_text)])])]
+    )
+    expected_json_path = get_expected_json_path(
+        FIXTURES_DIR, f"text_length_{text_length}.json"
+    )
+    result = format_adaptive_card(message_body)
+    assert_expected_json(result, expected_json_path)
diff --git a/tests/unit/messages/utils.py b/tests/unit/messages/utils.py
new file mode 100644
index 000000000..9805eb6e1
--- /dev/null
+++ b/tests/unit/messages/utils.py
@@ -0,0 +1,28 @@
+import json
+import os
+from pathlib import Path
+
+from elementary.utils.log import get_logger
+
+logger = get_logger(__name__)
+
+
+# Set to True to override the expected JSON files with the actual results
+# This is useful for updating the expected JSON files with the actual results, for development purposes only!
+OVERRIDE = os.getenv("OVERRIDE", "false").lower() == "true"
+
+
+def get_expected_json_path(fixture_dir: Path, filename: str) -> Path:
+    path = fixture_dir / filename
+    if not path.exists():
+        path.write_text(json.dumps({}))
+    return path
+
+
+def assert_expected_json(result: dict, expected_json_path: Path) -> None:
+    if OVERRIDE:
+        logger.warning(f"Overriding expected JSON file: {expected_json_path}")
+        print("writing to file", OVERRIDE)
+        expected_json_path.write_text(json.dumps(result, indent=2))
+    expected = json.loads(expected_json_path.read_text())
+    assert json.dumps(result, indent=2) == json.dumps(expected, indent=2)