#!/usr/bin/env python3
"""Verify the supporting public contracts added by Phase 4 documentation coverage."""
from __future__ import annotations

import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
API = json.loads((ROOT / 'docs/_inventory/assembly-api/public-api-assembly.json').read_text(encoding='utf-8'))
CLS = json.loads((ROOT / 'eng/tests/Nexamas.UI.PublicApiClassificationBaseline.json').read_text(encoding='utf-8-sig'))
OUT = ROOT / 'docs/_inventory/phase4-supporting-contract-verification.json'

TYPES = {t['name']: t for t in API['types']}
CLASS = {(x['namespace'], x['name'], x['kind']): x['category'] for x in CLS['classifications']}
assertions = 0
failures: list[str] = []


def check(condition: bool, label: str) -> None:
    global assertions
    assertions += 1
    if not condition:
        failures.append(label)


def type_(name: str) -> dict:
    t = TYPES.get(name)
    check(t is not None, f'type exists: {name}')
    return t or {'members': [], 'namespace': '', 'kind': '', 'name': name}


def member(name: str, member_name: str, kind: str | None = None) -> bool:
    t = type_(name)
    return any(m['name'] == member_name and (kind is None or m['kind'] == kind) and m['access'] == 'Public' for m in t['members'])


def enum_values(name: str) -> set[str]:
    t = type_(name)
    return {m['name'] for m in t['members'] if m['kind'] == 'Field' and m['access'] == 'Public' and m['name'] != 'value__'}


def category(name: str) -> str:
    t = type_(name)
    if t.get('kind') == 'Delegate' and t.get('isNested'):
        return 'GeneratedDelegate'
    return CLASS.get((t.get('namespace'), t.get('name'), t.get('kind')), 'Unclassified')


def doc_contains(path: str, names: list[str]) -> None:
    text = (ROOT / path).read_text(encoding='utf-8')
    for name in names:
        check(f'`{name}`' in text or name in text, f'{path} documents {name}')

# All 53 former API-table-only types must occur in their assigned owner document.
GROUPS = {
    'docs/reference/application-facade-surface.md':
        'HostFocusLostEventHandler PointerDownEventHandler PointerLeftEventHandler PointerMovedEventHandler PointerUpEventHandler PointerWheelEventHandler'.split(),
    'docs/reference/controls/navigation-and-commands.md':
        'MASDropDownMenuItemIntent MASDropDownMenuModel MASDropDownMenuNode MASDropDownMenuNodeKind MASToolbarItemSizing MASToolbarSpacer MASTopBarCommandEventArgs MASTopBarWindowCommand MASTopBarWindowCommandEventArgs MASMenuButton'.split(),
    'docs/reference/product-controls-user-reference.md':
        'MASAgendaViewMode MASAppLayoutSnapshot MASDashboardGridWidgetSnapshot MASKanbanBoardSnapshot MASKanbanCardSnapshot MASKanbanColumnSnapshot MASMasterDetailItemSnapshot'.split(),
    'docs/reference/controls/applied-filters-and-chart-legends.md':
        ['MASAppliedFilterRemoveRequestedEventArgs'],
    'docs/reference/controls/file-explorer-property-grid-and-filter-builder.md':
        'MASFileExplorerItemActivatedEventArgs MASFileExplorerItemKind MASFileExplorerLocationKind MASFilePickerResultEventArgs MASFilePickerMode MASFilePickerOptions MASFilePickerResult MASFileSystemSelectionKinds'.split(),
    'docs/reference/controls/list-and-tree-views.md':
        'MASListViewChromeMode MASListViewColumnClickEventArgs MASListViewColumnTextAlign MASListViewEmptySpaceContextMenuRequestedEventArgs MASListViewInlineRenameCommittedEventArgs MASListViewItemContextMenuRequestedEventArgs MASListViewItemKind MASListViewSortDirection MASListTreeIconSizeMode'.split(),
    'docs/reference/icon-resolution-surface.md':
        'IMASIconResolver MASIconImageRole MASIconKind MASIconObjectKind MASIconPalette MASIconRequest MASIconResult'.split(),
    'docs/reference/theme-surface-surface.md':
        'MASSurfaceStrength MASCardVisualStyle MASThemeContext MASTypography MASTextStyle'.split(),
}
for path, names in GROUPS.items():
    doc_contains(path, names)
check(sum(len(v) for v in GROUPS.values()) == 53, 'reconciliation group contains exactly 53 types')
check(len({n for v in GROUPS.values() for n in v}) == 53, 'reconciliation group type names are unique')

# Generated MASApplicationWindowInput delegate signatures.
delegate_params = {
    'PointerMovedEventHandler': ['clientX', 'clientY'],
    'PointerDownEventHandler': ['clientX', 'clientY', 'button'],
    'PointerUpEventHandler': ['clientX', 'clientY', 'button'],
    'PointerWheelEventHandler': ['clientX', 'clientY', 'delta'],
    'PointerLeftEventHandler': [],
    'HostFocusLostEventHandler': [],
}
for name, expected in delegate_params.items():
    t = type_(name)
    check(category(name) == 'GeneratedDelegate', f'{name} generated-delegate classification')
    invoke = next((m for m in t['members'] if m['name'] == 'Invoke' and m['kind'] == 'Method'), None)
    check(invoke is not None, f'{name}.Invoke exists')
    actual = [p['name'] for p in (invoke or {}).get('parameters', [])]
    check(actual == expected, f'{name}.Invoke parameter contract')

# Drop-down, toolbar, TopBar, and compatibility contracts.
check(enum_values('MASDropDownMenuItemIntent') == {'Default', 'Danger'}, 'drop-down intents')
check(enum_values('MASDropDownMenuNodeKind') == {'Root', 'ActionItem', 'SubMenu', 'Separator'}, 'drop-down node kinds')
for m in ['Root', 'Action', 'SubMenu', 'Separator', 'HasChildren']:
    check(member('MASDropDownMenuNode', m), f'MASDropDownMenuNode.{m}')
check(enum_values('MASToolbarItemSizing') == {'Fixed', 'Fill'}, 'toolbar item sizing values')
check(member('MASToolbarSpacer', 'WithSize'), 'MASToolbarSpacer.WithSize')
check(enum_values('MASTopBarWindowCommand') == {'None', 'Minimize', 'ToggleMaximizeRestore', 'CloseWindow'}, 'TopBar window command values')
check(member('MASTopBarCommandEventArgs', 'CommandId', 'Property'), 'MASTopBarCommandEventArgs.CommandId')
check(member('MASTopBarWindowCommandEventArgs', 'Command', 'Property'), 'MASTopBarWindowCommandEventArgs.Command')
check(category('MASMenuButton') == 'Compat', 'MASMenuButton is Compat')
for m in ['WithText', 'WithMenu', 'WithAction', 'WithSeparator', 'WithSubMenu', 'WithSize', 'ShowMenu', 'CloseMenu']:
    check(member('MASMenuButton', m), f'MASMenuButton.{m}')

# Product-control mode/snapshot routes and classifications.
check(enum_values('MASAgendaViewMode') == {'Day', 'Week', 'Month'}, 'agenda view modes')
for n in ['MASAppLayoutSnapshot', 'MASDashboardGridWidgetSnapshot', 'MASKanbanBoardSnapshot', 'MASKanbanCardSnapshot', 'MASKanbanColumnSnapshot', 'MASMasterDetailItemSnapshot']:
    check(category(n) == 'Preview', f'{n} is Preview')
for owner, create, restore in [
    ('MASAppLayout', 'CreateLayoutSnapshot', 'RestoreLayoutSnapshot'),
    ('MASDashboardGrid', 'CreateLayoutSnapshot', 'RestoreLayoutSnapshot'),
    ('MASKanbanBoard', 'CreateBoardSnapshot', 'RestoreBoardSnapshot'),
    ('MASMasterDetailView', 'CreateViewSnapshot', 'RestoreViewSnapshot'),
]:
    check(member(owner, create), f'{owner}.{create}')
    check(member(owner, restore), f'{owner}.{restore}')

# File Explorer and picker contracts.
check(enum_values('MASFileExplorerItemKind') == {'Folder', 'FileItem'}, 'file explorer item kinds')
check(enum_values('MASFileExplorerLocationKind') == {'Unknown', 'VirtualRoot', 'DriveRoot', 'SpecialFolder', 'Folder', 'FileItem'}, 'file explorer location kinds')
check(member('MASFileExplorerItemActivatedEventArgs', 'Item', 'Property'), 'explorer activation Item')
check(enum_values('MASFilePickerMode') == {'OpenFile', 'SaveFile', 'PickTargets'}, 'file picker modes')
check(enum_values('MASFileSystemSelectionKinds') == {'None', 'FileItem', 'Folder', 'Drive', 'Special', 'All'}, 'file picker selection kinds')
for m in ['OpenFile', 'SaveFile', 'FolderPicker', 'MultiFolderPicker', 'DrivePicker', 'FolderAndDrivePicker', 'TargetsPicker', 'Clone', 'Normalize']:
    check(member('MASFilePickerOptions', m), f'MASFilePickerOptions.{m}')
for m in ['Cancelled', 'FromPath', 'FromPaths', 'Accepted', 'HasSelection', 'SelectedPath', 'SelectedPaths']:
    check(member('MASFilePickerResult', m), f'MASFilePickerResult.{m}')
check(member('MASFilePickerResultEventArgs', 'Result', 'Property'), 'MASFilePickerResultEventArgs.Result')

# List/tree enums and event payloads.
check(enum_values('MASListViewChromeMode') == {'Standalone', 'Embedded', 'Transparent'}, 'list chrome modes')
check(enum_values('MASListViewItemKind') == {'Normal', 'GroupHeader'}, 'list item kinds')
check(enum_values('MASListViewColumnTextAlign') == {'Left', 'Center', 'Right'}, 'column alignment values')
check(enum_values('MASListViewSortDirection') == {'None', 'Ascending', 'Descending'}, 'list sort directions')
check(enum_values('MASListTreeIconSizeMode') == {'Small', 'Medium', 'Large'}, 'list/tree icon size modes')
for n, props in {
    'MASListViewColumnClickEventArgs': ['ColumnIndex', 'Direction'],
    'MASListViewItemContextMenuRequestedEventArgs': ['Item', 'ItemIndex', 'PointPx', 'AnchorRectPx'],
    'MASListViewEmptySpaceContextMenuRequestedEventArgs': ['PointPx', 'AnchorRectPx'],
    'MASListViewInlineRenameCommittedEventArgs': ['Item', 'ItemIndex', 'OriginalText', 'NewText', 'Cancel', 'ErrorMessage'],
}.items():
    for prop in props:
        check(member(n, prop, 'Property'), f'{n}.{prop}')

# Icon resolver and image ownership boundaries.
check(category('IMASIconResolver') == 'Advanced', 'IMASIconResolver is Advanced')
check(member('IMASIconResolver', 'ResolveIcon'), 'IMASIconResolver.ResolveIcon')
for m in ['None', 'FromImage', 'FromMasIcon', 'HasIcon', 'HasMasIcon', 'HasSkImage', 'IconKind', 'Image', 'ImageRole']:
    check(member('MASIconResult', m), f'MASIconResult.{m}')
check(enum_values('MASIconImageRole') == {'Icon', 'Thumbnail'}, 'icon image roles')
for m in ['FileDefault', 'FolderDefault', 'DriveDefault', 'FromColor']:
    check(member('MASIconPalette', m), f'MASIconPalette.{m}')
icon_request_source = (ROOT / 'Component/Reusable/Visuals/Icons/IconSources/Core/MASIconRequest.vb').read_text(encoding='utf-8')
icon_result_source = (ROOT / 'Component/Reusable/Visuals/Icons/IconSources/Core/MASIconResult.vb').read_text(encoding='utf-8')
check('Friend Sub New(' in icon_request_source and 'Public Sub New(' not in icon_request_source, 'MASIconRequest has no public constructor')
check('Me.Image = image' in icon_result_source, 'MASIconResult.FromImage retains supplied image reference')
check('Implements IDisposable' not in icon_result_source, 'MASIconResult is not IDisposable')

# Theme/typography/rendering contracts.
check(enum_values('MASCardVisualStyle') == {'Default', 'Secondary'}, 'card visual styles')
check(enum_values('MASSurfaceStrength') == {'ThemeDefault', 'Level1', 'Level2', 'Level3', 'Level4', 'Level5', 'Level6'}, 'surface strength values')
check(category('MASSurfaceStrength') == 'Evidence', 'MASSurfaceStrength classification')
for m in ['Dpi', 'Theme', 'Typography']:
    check(member('MASThemeContext', m, 'Property'), f'MASThemeContext.{m}')
for m in ['GetPaint', 'CreatePaint', 'BaselineY', 'Dispose', 'Scale']:
    check(member('MASTypography', m), f'MASTypography.{m}')
check(enum_values('MASTextStyle') == {'Title', 'Body', 'Small', 'Micro', 'Button', 'TopBarTitle', 'FieldText', 'FieldPlaceholder', 'SelectionText'}, 'typography text styles')
typography_source = (ROOT / 'MASSystem/Interaction/TextInput/Values/Typography/MASTypography.vb').read_text(encoding='utf-8')
theme_context_source = (ROOT / 'MASSystem/Core/Framework/Theming/MASThemeContext.vb').read_text(encoding='utf-8')
check('Return basePaint.Clone()' in typography_source, 'CreatePaint returns a caller-owned clone')
check('Public Function GetPaint' in typography_source and 'Return CreatePaint(style, color)' in typography_source, 'GetPaint routes to caller-owned clone')
check('Friend Sub New(' in theme_context_source and 'Public Sub New(' not in theme_context_source, 'MASThemeContext has no public constructor')

result = {
    'schema': 'Nexamas.UI.DocumentationAudit.Phase4SupportingContracts/1.0',
    'status': 'PASS' if not failures else 'FAIL',
    'assertions': assertions,
    'failures': failures,
    'assemblyPublicApiHash': API['publicApiHash'],
    'reconciledTypeCount': sum(len(v) for v in GROUPS.values()),
}
OUT.write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8', newline='\n')
print(f"Assertions: {assertions - len(failures)} passed, {len(failures)} failed")
for failure in failures:
    print(f'FAIL: {failure}')
sys.exit(0 if not failures else 1)
