/**
 * @license
 * Copyright (c) ZesTest 2025-2030. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 *
 * This file may contain AI-assisted code.
 * See LICENSE and NOTICE files for details.
 */

/**
 * Tests for `src/proxy/control-proxy.ts`.
 *
 * @remarks
 * Validates the unified Proxy handler that calls `page.evaluate()` directly.
 * Inline mocks — no mock helper files.
 */
import type { Page } from '@playwright/test';
import { describe, expect, it, vi } from 'vitest';

import type { MethodExecutionResult } from '#bridge/bridge-types.js';
import type { InteractionStrategy } from '#bridge/interaction-strategies/strategy.js';
import { BridgeError } from '#core/errors/bridge-error.js';
import { ControlError } from '#core/errors/control-error.js';
import type { UI5ControlBase } from '#core/types/controls.js';
import type {
  ControlInfoFull,
  ControlMetadataResult,
  ControlProxyState,
} from '#proxy/control-proxy.js';
import { createControlProxy } from '#proxy/control-proxy.js';

/** Extended interface for testing dynamic method calls on the proxy. */
interface TestProxy extends UI5ControlBase {
  getText(): Promise<string>;
  setText(text: string): Promise<void>;
  getEnabled(): Promise<boolean>;
  press(): Promise<void>;
  enterText(text: string): Promise<void>;
  select(itemId: string): Promise<void>;
}

/**
 * Accesses a dynamic method on a proxy or chainable result and calls it.
 *
 * @remarks
 * Bypasses `noUncheckedIndexedAccess` by using Reflect.get + explicit cast.
 * This is safe in tests because the proxy guarantees a function for any string key.
 */
function chainCall(target: unknown, method: string, ...args: unknown[]): unknown {
  const fn = Reflect.get(target as object, method) as (...a: unknown[]) => unknown;
  return fn(...args);
}

function createMockPage(evaluateResult?: unknown): Page {
  return {
    evaluate: vi.fn().mockResolvedValue(evaluateResult),
  } as unknown as Page;
}

function createMockStrategy(): InteractionStrategy {
  return {
    name: 'mock',
    press: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
    enterText: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
    select: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
  };
}

function createTestState(overrides?: Partial<ControlProxyState>): ControlProxyState {
  return {
    id: 'saveBtn',
    controlType: 'sap.m.Button',
    methods: new Set(['getText', 'setText', 'getEnabled']),
    page: createMockPage(),
    interactionStrategy: createMockStrategy(),
    ...overrides,
  };
}

function resultOk(value: unknown, returnType = 'result'): MethodExecutionResult {
  return {
    success: true,
    returnType: returnType as MethodExecutionResult['returnType'],
    value,
    duration: 1,
  };
}

/** Casts proxy to record for dynamic property access in tests. */
function toRecord(proxy: UI5ControlBase): Record<string, unknown> {
  return proxy as unknown as Record<string, unknown>;
}

describe('control-proxy', () => {
  // ── Property access ──────────────────────────────────────────────
  describe('property access', () => {
    it('id returns control ID directly', () => {
      const proxy = createControlProxy(createTestState());
      expect(proxy.id).toBe('saveBtn');
    });

    it('controlType returns type directly', () => {
      const proxy = createControlProxy(createTestState());
      expect(proxy.controlType).toBe('sap.m.Button');
    });

    it('getId() returns async control ID', async () => {
      const proxy = createControlProxy(createTestState());
      expect(await proxy.getId()).toBe('saveBtn');
    });

    it('getControlType() returns async type string', async () => {
      const proxy = createControlProxy(createTestState());
      expect(await proxy.getControlType()).toBe('sap.m.Button');
    });
  });

  // ── Anti-thenable ────────────────────────────────────────────────
  describe('anti-thenable', () => {
    it('then returns undefined', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      expect(proxy['then']).toBeUndefined();
    });

    it('catch returns undefined', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      expect(proxy['catch']).toBeUndefined();
    });

    it('finally returns undefined', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      expect(proxy['finally']).toBeUndefined();
    });
  });

  // ── Blacklist enforcement ────────────────────────────────────────
  describe('blacklist enforcement', () => {
    it('static blacklisted method throws ControlError', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      // eslint-disable-next-line @typescript-eslint/dot-notation -- testing blacklisted access
      expect(() => proxy['constructor']).toThrow(/blacklisted/);
    });

    it('underscore-prefixed method throws', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      expect(() => proxy['_internal']).toThrow(/blacklisted/);
    });

    it('Render-suffixed method throws', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      expect(() => proxy['onAfterRender']).toThrow(/blacklisted/);
    });

    it('getAggregation bypasses blacklist', () => {
      const proxy = createControlProxy(createTestState());
      expect(typeof proxy.getAggregation).toBe('function');
    });
  });

  // ── Explicit interaction ─────────────────────────────────────────
  describe('explicit interaction', () => {
    it('press() delegates to interaction strategy', async () => {
      const pressFn = vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined);
      const strategy: InteractionStrategy = {
        name: 'mock',
        press: pressFn,
        enterText: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
        select: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
      };
      const page = createMockPage();
      const state = createTestState({ page, interactionStrategy: strategy });
      const proxy = createControlProxy(state) as TestProxy;

      await proxy.press();

      expect(pressFn).toHaveBeenCalledWith(page, 'saveBtn');
    });

    it('enterText() delegates to interaction strategy', async () => {
      const enterTextFn = vi
        .fn<(...args: unknown[]) => Promise<void>>()
        .mockResolvedValue(undefined);
      const strategy: InteractionStrategy = {
        name: 'mock',
        press: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
        enterText: enterTextFn,
        select: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
      };
      const page = createMockPage();
      const state = createTestState({ page, interactionStrategy: strategy });
      const proxy = createControlProxy(state) as TestProxy;

      await proxy.enterText('Hello');

      expect(enterTextFn).toHaveBeenCalledWith(page, 'saveBtn', 'Hello');
    });

    it('select() delegates to interaction strategy', async () => {
      const selectFn = vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined);
      const strategy: InteractionStrategy = {
        name: 'mock',
        press: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
        enterText: vi.fn<(...args: unknown[]) => Promise<void>>().mockResolvedValue(undefined),
        select: selectFn,
      };
      const page = createMockPage();
      const state = createTestState({ page, interactionStrategy: strategy });
      const proxy = createControlProxy(state) as TestProxy;

      await proxy.select('item1');

      expect(selectFn).toHaveBeenCalledWith(page, 'saveBtn', 'item1');
    });
  });

  // ── Dynamic method forwarding ────────────────────────────────────
  describe('dynamic method forwarding', () => {
    it('forwards method calls via page.evaluate with function-form', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk('Save'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      const result = await proxy.getText();

      expect(result).toBe('Save');
      expect(evaluateMock).toHaveBeenCalledOnce();
      // Function-form: page.evaluate receives (fn, args) not a string
      const [fn, args] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(typeof fn).toBe('function');
      expect(args).toEqual(
        expect.objectContaining({ controlId: 'saveBtn', methodName: 'getText' }),
      );
    });

    it('passes arguments through to the browser function params', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk(undefined, 'empty'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      await proxy.setText('New Text');

      const [, args] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(args).toEqual(
        expect.objectContaining({
          controlId: 'saveBtn',
          methodName: 'setText',
          args: ['New Text'],
        }),
      );
    });

    it('includes bridgeNs in evaluate params', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk('Save'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      await proxy.getText();

      const [, args] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(args).toHaveProperty('bridgeNs', '__praman_bridge');
    });

    it('caches forwarder function per method name', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      const fn1 = proxy['getText'];
      const fn2 = proxy['getText'];
      expect(fn1).toBe(fn2);
    });

    it('creates different forwarders for different methods', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      const getText = proxy['getText'];
      const getEnabled = proxy['getEnabled'];
      expect(getText).not.toBe(getEnabled);
    });
  });

  // ── 7-type return handling ───────────────────────────────────────
  describe('7-type return handling', () => {
    it('result → returns raw value', async () => {
      const page = createMockPage(resultOk('Save'));
      const proxy = createControlProxy(createTestState({ page })) as TestProxy;
      expect(await proxy.getText()).toBe('Save');
    });

    it('empty → returns empty array', async () => {
      const page = createMockPage({ success: true, returnType: 'empty', duration: 1 });
      const proxy = createControlProxy(createTestState({ page })) as TestProxy;
      await expect(proxy.setText('x')).resolves.toEqual([]);
    });

    it('none → returns undefined', async () => {
      const page = createMockPage({ success: true, returnType: 'none', duration: 1 });
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getCustom'] as () => Promise<unknown>;
      expect(await fn()).toBeUndefined();
    });

    it('unknown → returns undefined', async () => {
      const page = createMockPage({ success: true, returnType: 'unknown', duration: 1 });
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getCustom'] as () => Promise<unknown>;
      expect(await fn()).toBeUndefined();
    });

    it('element → returns sub-proxy with same type and methods', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'element',
        value: { id: 'saveBtn' },
        duration: 1,
      });
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getParent'] as () => Promise<unknown>;
      const subProxy = (await fn()) as UI5ControlBase;

      expect(subProxy.id).toBe('saveBtn');
      expect(subProxy.controlType).toBe('sap.m.Button');
    });

    it('newElement → returns sub-proxy with provided controlType', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'newElement',
        value: { id: 'newCtrl', controlType: 'sap.ui.table.Table' },
        duration: 1,
      });
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getChild'] as () => Promise<unknown>;
      const subProxy = (await fn()) as UI5ControlBase;

      expect(subProxy.id).toBe('newCtrl');
      expect(subProxy.controlType).toBe('sap.ui.table.Table');
    });

    it('newElement without controlType → falls back to unknown', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'newElement',
        value: { id: 'noTypeCtrl' },
        duration: 1,
      });
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getChild'] as () => Promise<unknown>;
      const subProxy = (await fn()) as UI5ControlBase;

      expect(subProxy.id).toBe('noTypeCtrl');
      expect(subProxy.controlType).toBe('unknown');
    });

    it('element with undefined value → returns undefined', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'element',
        value: undefined,
        duration: 1,
      });
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getParent'] as () => Promise<unknown>;
      expect(await fn()).toBeUndefined();
    });

    it('aggregation → returns array of control proxies', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'aggregation',
        uuids: ['item1', 'item2'],
        objectTypes: ['sap.m.StandardListItem', 'sap.m.StandardListItem'],
        duration: 1,
      });
      const proxy = createControlProxy(createTestState({ page }));
      const items = (await proxy.getAggregation('items')) as UI5ControlBase[];

      expect(items).toHaveLength(2);
      expect(items[0]?.id).toBe('item1');
      expect(items[0]?.controlType).toBe('sap.m.StandardListItem');
      expect(items[1]?.id).toBe('item2');
    });

    it('aggregation with empty arrays → returns empty array', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'aggregation',
        uuids: [],
        objectTypes: [],
        duration: 1,
      });
      const proxy = createControlProxy(createTestState({ page }));
      const items = await proxy.getAggregation('items');
      expect(items).toHaveLength(0);
    });

    it('element sub-proxy inherits skipStabilityWait flag', async () => {
      const evaluateMock = vi.fn();
      // First call: getParent → returns element sub-proxy
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'element',
        value: { id: 'parentBtn' },
        duration: 1,
      } satisfies MethodExecutionResult);
      // Second call: getText on sub-proxy
      evaluateMock.mockResolvedValueOnce(resultOk('Parent Text'));

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page, skipStabilityWait: true });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getParent'] as () => Promise<unknown>;
      const subProxy = (await fn()) as UI5ControlBase;

      // Sub-proxy should exist and be functional
      expect(subProxy.id).toBe('parentBtn');
      // Verify the sub-proxy was created with skipStabilityWait
      // We verify indirectly: the sub-proxy should pass skipStabilityWait to its own sub-proxies
      // Direct verification: check the state was propagated by reading the proxy's behavior
      expect(subProxy).toBeDefined();
    });

    it('newElement sub-proxy inherits skipStabilityWait flag', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'newElement',
        value: { id: 'newCtrl', controlType: 'sap.ui.table.Table' },
        duration: 1,
      } satisfies MethodExecutionResult);
      evaluateMock.mockResolvedValueOnce(resultOk('New Text'));

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page, skipStabilityWait: true });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getChild'] as () => Promise<unknown>;
      const subProxy = (await fn()) as UI5ControlBase;

      expect(subProxy.id).toBe('newCtrl');
      expect(subProxy.controlType).toBe('sap.ui.table.Table');
    });

    it('aggregation sub-proxies inherit skipStabilityWait flag', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'aggregation',
        uuids: ['item1', 'item2'],
        objectTypes: ['sap.m.StandardListItem', 'sap.m.StandardListItem'],
        duration: 1,
      } satisfies MethodExecutionResult);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page, skipStabilityWait: true });
      const proxy = createControlProxy(state);
      const items = (await proxy.getAggregation('items')) as UI5ControlBase[];

      expect(items).toHaveLength(2);
      expect(items[0]?.id).toBe('item1');
      expect(items[1]?.id).toBe('item2');
    });

    it('skipStabilityWait defaults to undefined when not set', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'element',
        value: { id: 'childBtn' },
        duration: 1,
      });
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getParent'] as () => Promise<unknown>;
      const subProxy = (await fn()) as UI5ControlBase;

      // Sub-proxy should still work when skipStabilityWait is not set
      expect(subProxy.id).toBe('childBtn');
    });

    it('object → returns UI5Object proxy', async () => {
      const evaluateMock = vi.fn();
      // First call: control method execution (getModel)
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'object',
        uuid: 'model-uuid-1',
        objectType: 'sap.ui.model.json.JSONModel',
        duration: 1,
      });
      // Second call: UI5Object.create -> loadMethods
      evaluateMock.mockResolvedValueOnce(['getData', 'setData']);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModel'] as () => Promise<unknown>;
      const result = (await fn()) as Record<string, unknown>;

      expect(result['uuid']).toBe('model-uuid-1');
      expect(result['type']).toBe('sap.ui.model.json.JSONModel');
    });

    it('failure → throws BridgeError', async () => {
      const page = createMockPage({
        success: false,
        returnType: 'none',
        error: 'Control not found: saveBtn',
        duration: 1,
      });
      const proxy = createControlProxy(createTestState({ page })) as TestProxy;
      await expect(proxy.getText()).rejects.toThrow('Control not found: saveBtn');
    });

    it('failure without error message → throws generic BridgeError', async () => {
      const page = createMockPage({
        success: false,
        returnType: 'none',
        duration: 1,
      });
      const proxy = createControlProxy(createTestState({ page })) as TestProxy;
      await expect(proxy.getText()).rejects.toThrow('Bridge method execution failed');
    });
  });

  // ── Proxy behavior ──────────────────────────────────────────────
  describe('proxy behavior', () => {
    it('is not extensible', () => {
      const proxy = createControlProxy(createTestState());
      expect(Object.isExtensible(proxy)).toBe(false);
    });

    it('toString() returns display string', () => {
      const proxy = createControlProxy(createTestState());
      const toStr = Reflect.get(proxy, 'toString') as () => string;
      expect(toStr()).toBe('[UI5Control sap.m.Button#saveBtn]');
    });

    it('toJSON() returns display string', () => {
      const proxy = createControlProxy(createTestState());
      const toJson = Reflect.get(proxy, 'toJSON') as () => string;
      expect(toJson()).toBe('[UI5Control sap.m.Button#saveBtn]');
    });

    it('Symbol.toPrimitive returns display string', () => {
      const proxy = createControlProxy(createTestState());
      const toPrimitive = Reflect.get(proxy, Symbol.toPrimitive) as () => string;
      expect(toPrimitive()).toBe('[UI5Control sap.m.Button#saveBtn]');
    });

    it('unknown symbol returns undefined', () => {
      const proxy = createControlProxy(createTestState());
      expect(Reflect.get(proxy, Symbol.iterator)).toBeUndefined();
    });

    it('preventExtensions returns true', () => {
      const proxy = createControlProxy(createTestState());
      expect(Object.preventExtensions(proxy)).toBe(proxy);
    });
  });

  // ── Fluent proxy / method chaining (GAP-10) ──────────────────────
  describe('fluent proxy chaining', () => {
    it('getParent().getText() chains correctly', async () => {
      const evaluateMock = vi.fn();
      // First call: getParent → returns element sub-proxy
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'element',
        value: { id: 'parentBtn' },
        duration: 1,
      } satisfies MethodExecutionResult);
      // Second call: getText on the sub-proxy → returns result
      evaluateMock.mockResolvedValueOnce(resultOk('Parent Text'));

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      // Chain: proxy.getParent().getText()
      const parentResult = chainCall(proxy, 'getParent');
      const result = await (chainCall(parentResult, 'getText') as Promise<unknown>);

      expect(result).toBe('Parent Text');
      expect(evaluateMock).toHaveBeenCalledTimes(2);
    });

    it('chain terminates gracefully on primitive result', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk('just a string'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      // Chain: proxy.getText().something() — getText returns a primitive string
      const textResult = chainCall(proxy, 'getText');
      const result = await (chainCall(textResult, 'something') as Promise<unknown>);

      expect(result).toBeUndefined();
    });

    it('chain supports triple depth', async () => {
      const evaluateMock = vi.fn();
      // First call: getParent → returns element sub-proxy (parent)
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'element',
        value: { id: 'parentCtrl' },
        duration: 1,
      } satisfies MethodExecutionResult);
      // Second call: getParent on parent → returns element sub-proxy (grandparent)
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'element',
        value: { id: 'grandparentCtrl' },
        duration: 1,
      } satisfies MethodExecutionResult);
      // Third call: getText on grandparent → returns result
      evaluateMock.mockResolvedValueOnce(resultOk('Root Text'));

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      // Chain: proxy.getParent().getParent().getText()
      const parent = chainCall(proxy, 'getParent');
      const grandparent = chainCall(parent, 'getParent');
      const result = await (chainCall(grandparent, 'getText') as Promise<unknown>);

      expect(result).toBe('Root Text');
      expect(evaluateMock).toHaveBeenCalledTimes(3);
    });

    it('await on chainable result works like normal promise', async () => {
      const page = createMockPage(resultOk('Save'));
      const proxy = createControlProxy(createTestState({ page })) as TestProxy;
      // Standard await still works — backward compatibility
      const text = await proxy.getText();
      expect(text).toBe('Save');
    });

    it('chainable result works with catch', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('network error'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      const rec = toRecord(proxy);
      const getText = rec['getText'] as (...args: unknown[]) => unknown;
      const chainable = getText();

      await expect(chainable as Promise<unknown>).rejects.toThrow('network error');
    });

    it('chain does not affect direct property access', () => {
      const proxy = createControlProxy(createTestState());
      // Direct properties remain synchronous — not wrapped in chainable
      expect(proxy.id).toBe('saveBtn');
      expect(proxy.controlType).toBe('sap.m.Button');
    });
  });

  // ── exec() (GAP-12) ─────────────────────────────────────────────
  describe('exec()', () => {
    it('calls page.evaluate with serialized function and control ID', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(42);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const execFn = proxy['exec'] as (
        fn: (ctrl: unknown) => unknown,
        ...args: unknown[]
      ) => Promise<unknown>;

      const userFn = (ctrl: unknown): unknown => (ctrl as Record<string, unknown>)['getText'];
      const result = await execFn(userFn);

      expect(result).toBe(42);
      expect(evaluateMock).toHaveBeenCalledOnce();
    });

    it('passes additional arguments to page.evaluate params', async () => {
      const evaluateMock = vi.fn().mockResolvedValue('computed');
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const execFn = proxy['exec'] as (
        fn: (ctrl: unknown, ...a: unknown[]) => unknown,
        ...args: unknown[]
      ) => Promise<unknown>;

      const userFn = (_ctrl: unknown, multiplier: unknown): unknown => multiplier;
      await execFn(userFn, 5);

      const [, params] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(params['execArgs']).toEqual([5]);
    });

    it('includes controlId and bridgeNs in evaluate params', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(null);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const execFn = proxy['exec'] as (fn: (ctrl: unknown) => unknown) => Promise<unknown>;

      await execFn((ctrl: unknown): unknown => ctrl);

      const [, params] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(params['controlId']).toBe('saveBtn');
      expect(params['bridgeNs']).toBe('__praman_bridge');
    });

    it('serializes the function body as fnBody param', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(undefined);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const execFn = proxy['exec'] as (fn: (ctrl: unknown) => unknown) => Promise<unknown>;

      const userFn = (ctrl: unknown): unknown => ctrl;
      await execFn(userFn);

      const [, params] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(params['fnBody']).toBe(userFn.toString());
    });

    it('returns the raw result from page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue({ key: 'value' });
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const execFn = proxy['exec'] as (fn: (ctrl: unknown) => unknown) => Promise<unknown>;

      const result = await execFn((ctrl: unknown): unknown => ctrl);

      expect(result).toEqual({ key: 'value' });
    });

    it('propagates errors from page.evaluate', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('Browser error'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const execFn = proxy['exec'] as (fn: (ctrl: unknown) => unknown) => Promise<unknown>;

      await expect(execFn((ctrl: unknown): unknown => ctrl)).rejects.toThrow('Browser error');
    });
  });

  // ── renewWebElementReference (GAP-13) ────────────────────────────
  describe('renewWebElementReference()', () => {
    it('resolves successfully when control exists in registry', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(true);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const renewFn = proxy['renewWebElementReference'] as () => Promise<void>;

      await expect(renewFn()).resolves.toBeUndefined();
    });

    it('throws ControlError when control no longer exists', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(false);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const renewFn = proxy['renewWebElementReference'] as () => Promise<void>;

      await expect(renewFn()).rejects.toThrow(/no longer exists/);
    });

    it('includes control ID in error message', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(false);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page, id: 'myBtn' });
      const proxy = toRecord(createControlProxy(state));
      const renewFn = proxy['renewWebElementReference'] as () => Promise<void>;

      await expect(renewFn()).rejects.toThrow('myBtn');
    });

    it('passes controlId and bridgeNs to page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(true);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const renewFn = proxy['renewWebElementReference'] as () => Promise<void>;

      await renewFn();

      const [, params] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(params['controlId']).toBe('saveBtn');
      expect(params['bridgeNs']).toBe('__praman_bridge');
    });

    it('throws ControlError with correct error code', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(false);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const renewFn = proxy['renewWebElementReference'] as () => Promise<void>;

      try {
        await renewFn();
        expect.fail('Should have thrown');
      } catch (error: unknown) {
        expect((error as Record<string, unknown>)['code']).toBe('ERR_CONTROL_NOT_FOUND');
      }
    });

    it('propagates errors from page.evaluate', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('Page crashed'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const renewFn = proxy['renewWebElementReference'] as () => Promise<void>;

      await expect(renewFn()).rejects.toThrow('Page crashed');
    });
  });

  // ── Control introspection methods (GAP-16) ────────────────────────
  describe('getControlMetadata()', () => {
    it('returns metadata with className, properties, aggregations, events', async () => {
      const metadataResult: ControlMetadataResult = {
        className: 'sap.m.Button',
        properties: ['text', 'enabled', 'type', 'icon'],
        aggregations: ['customData', 'tooltip'],
        events: ['press', 'tap'],
      };
      const evaluateMock = vi.fn().mockResolvedValue(metadataResult);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlMetadata'] as () => Promise<ControlMetadataResult>;

      const result = await fn();

      expect(result.className).toBe('sap.m.Button');
      expect(result.properties).toEqual(['text', 'enabled', 'type', 'icon']);
      expect(result.aggregations).toEqual(['customData', 'tooltip']);
      expect(result.events).toEqual(['press', 'tap']);
    });

    it('passes controlId and bridgeNs to page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue({
        className: 'sap.m.Button',
        properties: [],
        aggregations: [],
        events: [],
      });
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlMetadata'] as () => Promise<ControlMetadataResult>;

      await fn();

      expect(evaluateMock).toHaveBeenCalledOnce();
      const [, params] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(params['controlId']).toBe('saveBtn');
      expect(params['bridgeNs']).toBe('__praman_bridge');
    });

    it('uses function-form page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue({
        className: 'sap.m.Button',
        properties: [],
        aggregations: [],
        events: [],
      });
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlMetadata'] as () => Promise<ControlMetadataResult>;

      await fn();

      const [browserFn] = evaluateMock.mock.calls[0] as [unknown];
      expect(typeof browserFn).toBe('function');
    });

    it('propagates errors from page.evaluate', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('Bridge not initialized'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlMetadata'] as () => Promise<ControlMetadataResult>;

      await expect(fn()).rejects.toThrow('Bridge not initialized');
    });
  });

  describe('getControlInfoFull()', () => {
    it('returns full info with id, controlType, methods, properties, aggregations, events', async () => {
      const infoResult: ControlInfoFull = {
        id: 'saveBtn',
        controlType: 'sap.m.Button',
        methods: ['getText', 'setText', 'getEnabled', 'setEnabled'],
        properties: ['text', 'enabled', 'type', 'icon'],
        aggregations: ['customData', 'tooltip'],
        events: ['press', 'tap'],
      };
      const evaluateMock = vi.fn().mockResolvedValue(infoResult);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlInfoFull'] as () => Promise<ControlInfoFull>;

      const result = await fn();

      expect(result.id).toBe('saveBtn');
      expect(result.controlType).toBe('sap.m.Button');
      expect(result.methods).toEqual(['getText', 'setText', 'getEnabled', 'setEnabled']);
      expect(result.properties).toEqual(['text', 'enabled', 'type', 'icon']);
      expect(result.aggregations).toEqual(['customData', 'tooltip']);
      expect(result.events).toEqual(['press', 'tap']);
    });

    it('passes controlId and bridgeNs to page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue({
        id: 'saveBtn',
        controlType: 'sap.m.Button',
        methods: [],
        properties: [],
        aggregations: [],
        events: [],
      });
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlInfoFull'] as () => Promise<ControlInfoFull>;

      await fn();

      expect(evaluateMock).toHaveBeenCalledOnce();
      const [, params] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(params['controlId']).toBe('saveBtn');
      expect(params['bridgeNs']).toBe('__praman_bridge');
    });

    it('uses function-form page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue({
        id: 'saveBtn',
        controlType: 'sap.m.Button',
        methods: [],
        properties: [],
        aggregations: [],
        events: [],
      });
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlInfoFull'] as () => Promise<ControlInfoFull>;

      await fn();

      const [browserFn] = evaluateMock.mock.calls[0] as [unknown];
      expect(typeof browserFn).toBe('function');
    });

    it('propagates errors from page.evaluate', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('Control not found'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['getControlInfoFull'] as () => Promise<ControlInfoFull>;

      await expect(fn()).rejects.toThrow('Control not found');
    });
  });

  describe('retrieveMembers()', () => {
    it('returns array of method names', async () => {
      const methods = ['getText', 'setText', 'getEnabled', 'setEnabled', 'getIcon', 'setIcon'];
      const evaluateMock = vi.fn().mockResolvedValue(methods);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['retrieveMembers'] as () => Promise<readonly string[]>;

      const result = await fn();

      expect(result).toEqual(methods);
    });

    it('passes controlId and bridgeNs to page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue([]);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['retrieveMembers'] as () => Promise<readonly string[]>;

      await fn();

      expect(evaluateMock).toHaveBeenCalledOnce();
      const [, params] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(params['controlId']).toBe('saveBtn');
      expect(params['bridgeNs']).toBe('__praman_bridge');
    });

    it('uses function-form page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue([]);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['retrieveMembers'] as () => Promise<readonly string[]>;

      await fn();

      const [browserFn] = evaluateMock.mock.calls[0] as [unknown];
      expect(typeof browserFn).toBe('function');
    });

    it('returns empty array when control has no methods', async () => {
      const evaluateMock = vi.fn().mockResolvedValue([]);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['retrieveMembers'] as () => Promise<readonly string[]>;

      const result = await fn();

      expect(result).toEqual([]);
    });

    it('propagates errors from page.evaluate', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('Bridge not available'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = toRecord(createControlProxy(state));
      const fn = proxy['retrieveMembers'] as () => Promise<readonly string[]>;

      await expect(fn()).rejects.toThrow('Bridge not available');
    });
  });

  // ── objectArray return type (GAP-11) ──────────────────────────────
  describe('objectArray return type', () => {
    it('objectArray → returns array of UI5Object proxies', async () => {
      const evaluateMock = vi.fn();
      // First call: method execution returns objectArray
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'objectArray',
        uuids: ['uuid-1', 'uuid-2'],
        objectTypes: ['sap.ui.model.json.JSONModel', 'sap.ui.model.odata.v4.ODataModel'],
        duration: 1,
      } satisfies MethodExecutionResult);
      // Second call: UI5Object.create -> loadMethods for uuid-1
      evaluateMock.mockResolvedValueOnce(['getData', 'setData']);
      // Third call: UI5Object.create -> loadMethods for uuid-2
      evaluateMock.mockResolvedValueOnce(['read', 'create']);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModels'] as () => Promise<unknown[]>;
      const result = await fn();

      expect(result).toHaveLength(2);
      expect((result[0] as Record<string, unknown>)['uuid']).toBe('uuid-1');
      expect((result[0] as Record<string, unknown>)['type']).toBe('sap.ui.model.json.JSONModel');
      expect((result[1] as Record<string, unknown>)['uuid']).toBe('uuid-2');
      expect((result[1] as Record<string, unknown>)['type']).toBe(
        'sap.ui.model.odata.v4.ODataModel',
      );
    });

    it('objectArray with empty uuid → returns null for that item', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'objectArray',
        uuids: ['uuid-1', ''],
        objectTypes: ['sap.ui.model.json.JSONModel', 'string'],
        duration: 1,
      } satisfies MethodExecutionResult);
      // Only uuid-1 triggers UI5Object.create -> loadMethods
      evaluateMock.mockResolvedValueOnce(['getData']);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModels'] as () => Promise<unknown[]>;
      const result = await fn();

      expect(result).toHaveLength(2);
      expect((result[0] as Record<string, unknown>)['uuid']).toBe('uuid-1');
      expect(result[1]).toBeNull();
    });

    it('objectArray with empty arrays → returns empty array', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'objectArray',
        uuids: [],
        objectTypes: [],
        duration: 1,
      } satisfies MethodExecutionResult);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModels'] as () => Promise<unknown[]>;
      const result = await fn();

      expect(result).toHaveLength(0);
    });

    it('objectArray with missing uuids/objectTypes → defaults to empty', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'objectArray',
        duration: 1,
      } satisfies MethodExecutionResult);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModels'] as () => Promise<unknown[]>;
      const result = await fn();

      expect(result).toHaveLength(0);
    });

    it('objectArray with missing objectTypes falls back to unknown', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'objectArray',
        uuids: ['uuid-1'],
        // objectTypes intentionally omitted
        duration: 1,
      } satisfies MethodExecutionResult);
      evaluateMock.mockResolvedValueOnce([]);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModels'] as () => Promise<unknown[]>;
      const result = await fn();

      expect(result).toHaveLength(1);
      expect((result[0] as Record<string, unknown>)['type']).toBe('unknown');
    });
  });

  // ── Context-destroyed retry logic ──────────────────────────────────
  describe('context-destroyed retry logic', () => {
    it('retries on "Execution context was destroyed" and succeeds on retry', async () => {
      vi.useFakeTimers();
      try {
        const evaluateMock = vi.fn();
        // First call: context destroyed
        evaluateMock.mockRejectedValueOnce(new Error('Execution context was destroyed'));
        // Second call: success
        evaluateMock.mockResolvedValueOnce(resultOk('Retry Success'));

        const page = { evaluate: evaluateMock } as unknown as Page;
        const state = createTestState({ page });
        const proxy = createControlProxy(state) as TestProxy;

        const promise = proxy.getText();
        // Advance past the retry delay (2500ms)
        await vi.advanceTimersByTimeAsync(3000);
        const result = await promise;

        expect(result).toBe('Retry Success');
        expect(evaluateMock).toHaveBeenCalledTimes(2);
      } finally {
        vi.useRealTimers();
      }
    });

    it('retries up to MAX_CONTEXT_RETRIES (3) times then throws BridgeError', async () => {
      vi.useFakeTimers();
      try {
        const contextError = new Error('Execution context was destroyed');
        const evaluateMock = vi.fn().mockRejectedValue(contextError);

        const page = { evaluate: evaluateMock } as unknown as Page;
        const state = createTestState({ page });
        const proxy = createControlProxy(state) as TestProxy;

        // eslint-disable-next-line promise/prefer-await-to-then -- must attach handler before timer advance to prevent unhandled rejection
        const promise = proxy.getText().catch((err: unknown) => err);
        // Advance past first retry delay
        await vi.advanceTimersByTimeAsync(3000);
        // Advance past second retry delay
        await vi.advanceTimersByTimeAsync(3000);
        // Advance past potential extra to ensure settlement
        await vi.advanceTimersByTimeAsync(3000);

        const error = await promise;
        expect(error).toBeInstanceOf(BridgeError);
        expect((error as BridgeError).code).toBe('ERR_BRIDGE_EXECUTION');
        expect((error as BridgeError).retryable).toBe(false);
        expect(evaluateMock).toHaveBeenCalledTimes(3);
      } finally {
        vi.useRealTimers();
      }
    });

    it('does not retry for non-context-destroyed errors and wraps in BridgeError', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('Some other error'));

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Expected getText to throw');
      } catch (error: unknown) {
        expect(error).toBeInstanceOf(BridgeError);
        const bridgeError = error as BridgeError;
        expect(bridgeError.code).toBe('ERR_BRIDGE_EXECUTION');
        expect(bridgeError.message).toContain('Some other error');
      }
      expect(evaluateMock).toHaveBeenCalledTimes(1);
    });

    it('does not retry for non-Error rejections and wraps in BridgeError without cause', async () => {
      const evaluateMock = vi.fn().mockRejectedValue('string rejection');

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Expected getText to throw');
      } catch (error: unknown) {
        expect(error).toBeInstanceOf(BridgeError);
        const bridgeError = error as BridgeError;
        expect(bridgeError.code).toBe('ERR_BRIDGE_EXECUTION');
        expect(bridgeError.cause).toBeUndefined();
      }
      expect(evaluateMock).toHaveBeenCalledTimes(1);
    });

    it('succeeds on the third attempt after two context-destroyed errors', async () => {
      vi.useFakeTimers();
      try {
        const contextError = new Error('Execution context was destroyed');
        const evaluateMock = vi
          .fn()
          .mockRejectedValueOnce(contextError)
          .mockRejectedValueOnce(contextError)
          .mockResolvedValueOnce(resultOk('Third Time'));

        const page = { evaluate: evaluateMock } as unknown as Page;
        const state = createTestState({ page });
        const proxy = createControlProxy(state) as TestProxy;

        const promise = proxy.getText();
        // Advance past the first retry delay
        await vi.advanceTimersByTimeAsync(3000);
        // Advance past the second retry delay
        await vi.advanceTimersByTimeAsync(3000);
        const result = await promise;

        expect(result).toBe('Third Time');
        expect(evaluateMock).toHaveBeenCalledTimes(3);
      } finally {
        vi.useRealTimers();
      }
    });
  });

  // ── page.evaluate error wrapping (P33) ──────────────────────────────
  describe('page.evaluate error wrapping', () => {
    it('wraps generic Error from page.evaluate in BridgeError with ERR_BRIDGE_EXECUTION', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('test browser error'));

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Expected getText to throw');
      } catch (error: unknown) {
        expect(error).toBeInstanceOf(BridgeError);
        const bridgeError = error as BridgeError;
        expect(bridgeError.code).toBe('ERR_BRIDGE_EXECUTION');
      }
    });

    it('BridgeError wrapper includes cause chain to original error', async () => {
      const originalError = new Error('original evaluate failure');
      const evaluateMock = vi.fn().mockRejectedValue(originalError);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Expected getText to throw');
      } catch (error: unknown) {
        expect(error).toBeInstanceOf(BridgeError);
        const bridgeError = error as BridgeError;
        expect(bridgeError.cause).toBeInstanceOf(Error);
        expect((bridgeError.cause as Error).message).toBe('original evaluate failure');
      }
    });

    it('passes through existing BridgeError without double-wrapping', async () => {
      const existingBridgeError = new BridgeError({
        code: 'ERR_BRIDGE_NOT_READY',
        message: 'Bridge not ready',
        attempted: 'test',
        retryable: true,
        suggestions: [],
      });
      const evaluateMock = vi.fn().mockRejectedValue(existingBridgeError);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Expected getText to throw');
      } catch (error: unknown) {
        expect(error).toBe(existingBridgeError); // Same reference, not wrapped
      }
    });

    it('passes through existing ControlError without double-wrapping', async () => {
      const existingControlError = new ControlError({
        code: 'ERR_CONTROL_NOT_FOUND',
        message: 'Control not found',
        attempted: 'test',
        suggestions: [],
      });
      const evaluateMock = vi.fn().mockRejectedValue(existingControlError);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Expected getText to throw');
      } catch (error: unknown) {
        expect(error).toBe(existingControlError); // Same reference, not wrapped
      }
    });

    it('wraps non-Error rejection in BridgeError without cause', async () => {
      const evaluateMock = vi.fn().mockRejectedValue('string error value');

      const page = { evaluate: evaluateMock } as unknown as Page;
      const state = createTestState({ page });
      const proxy = createControlProxy(state) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Expected getText to throw');
      } catch (error: unknown) {
        expect(error).toBeInstanceOf(BridgeError);
        const bridgeError = error as BridgeError;
        expect(bridgeError.cause).toBeUndefined();
        expect(bridgeError.message).toContain('string error value');
      }
    });
  });

  // ── handleReturn edge cases ────────────────────────────────────────
  describe('handleReturn edge cases', () => {
    it('object with missing uuid defaults to empty string', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'object',
        // uuid intentionally omitted
        objectType: 'sap.ui.model.json.JSONModel',
        duration: 1,
      } satisfies MethodExecutionResult);
      // UI5Object.create -> loadMethods
      evaluateMock.mockResolvedValueOnce([]);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModel'] as () => Promise<unknown>;
      const result = (await fn()) as Record<string, unknown>;

      expect(result['uuid']).toBe('');
    });

    it('object with missing objectType defaults to unknown', async () => {
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'object',
        uuid: 'model-uuid-1',
        // objectType intentionally omitted
        duration: 1,
      } satisfies MethodExecutionResult);
      // UI5Object.create -> loadMethods
      evaluateMock.mockResolvedValueOnce([]);

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getModel'] as () => Promise<unknown>;
      const result = (await fn()) as Record<string, unknown>;

      expect(result['type']).toBe('unknown');
    });

    it('aggregation with missing uuids defaults to empty array', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'aggregation',
        // uuids intentionally omitted
        duration: 1,
      });
      const proxy = createControlProxy(createTestState({ page }));
      const items = await proxy.getAggregation('items');
      expect(items).toHaveLength(0);
    });

    it('aggregation with more uuids than types uses unknown for excess', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'aggregation',
        uuids: ['item1', 'item2', 'item3'],
        objectTypes: ['sap.m.StandardListItem'],
        duration: 1,
      });
      const proxy = createControlProxy(createTestState({ page }));
      const items = (await proxy.getAggregation('items')) as UI5ControlBase[];

      expect(items).toHaveLength(3);
      expect(items[0]?.controlType).toBe('sap.m.StandardListItem');
      expect(items[1]?.controlType).toBe('unknown');
      expect(items[2]?.controlType).toBe('unknown');
    });

    it('newElement with undefined value returns undefined', async () => {
      const page = createMockPage({
        success: true,
        returnType: 'newElement',
        value: undefined,
        duration: 1,
      });
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['getChild'] as () => Promise<unknown>;
      expect(await fn()).toBeUndefined();
    });

    it('failure result includes BridgeError code', async () => {
      const page = createMockPage({
        success: false,
        returnType: 'none',
        error: 'Some failure',
        duration: 1,
      });
      const proxy = createControlProxy(createTestState({ page })) as TestProxy;

      try {
        await proxy.getText();
        expect.fail('Should have thrown');
      } catch (error: unknown) {
        expect((error as Record<string, unknown>)['code']).toBe('ERR_BRIDGE_EXECUTION');
      }
    });
  });

  // ── createChainableResult deep coverage ────────────────────────────
  describe('createChainableResult edge cases', () => {
    it('symbol access on chainable delegates to the underlying promise', () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk('Hello'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      const rec = toRecord(proxy);
      const getText = rec['getText'] as (...args: unknown[]) => unknown;
      const chainable = getText();

      // Symbol.toStringTag on a Promise yields 'Promise'
      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Reflect.get returns any for symbol property access
      const tag = Reflect.get(chainable as object, Symbol.toStringTag);
      expect(tag).toBe('Promise');
    });

    it('finally on chainable works like promise finally', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk('Done'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      const rec = toRecord(proxy);
      const getText = rec['getText'] as (...args: unknown[]) => unknown;
      const chainable = getText();

      let finallyCalled = false;
      const chainableObj = chainable as { finally: (fn: () => void) => Promise<unknown> };
      await chainableObj.finally(() => {
        finallyCalled = true;
      });
      expect(finallyCalled).toBe(true);
    });

    it('catch on chainable works like promise catch', async () => {
      const evaluateMock = vi.fn().mockRejectedValue(new Error('fail'));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      const rec = toRecord(proxy);
      const getText = rec['getText'] as (...args: unknown[]) => unknown;
      const chainable = getText();

      let catchCalled = false;
      const chainableObj = chainable as { catch: (fn: (err: unknown) => void) => Promise<unknown> };
      await chainableObj.catch(() => {
        catchCalled = true;
      });
      expect(catchCalled).toBe(true);
    });

    it('chain with null resolved value terminates gracefully', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk(null));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      const result1 = chainCall(proxy, 'getValue');
      const result2 = await (chainCall(result1, 'something') as Promise<unknown>);

      expect(result2).toBeUndefined();
    });

    it('chain with resolved object that has the method calls it', async () => {
      // First call returns an element (sub-proxy), second call getText on it
      const evaluateMock = vi.fn();
      evaluateMock.mockResolvedValueOnce({
        success: true,
        returnType: 'element',
        value: { id: 'innerCtrl' },
        duration: 1,
      } satisfies MethodExecutionResult);
      evaluateMock.mockResolvedValueOnce(resultOk('Inner Text'));

      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      const parent = chainCall(proxy, 'getParent');
      const text = await (chainCall(parent, 'getText') as Promise<unknown>);

      expect(text).toBe('Inner Text');
    });

    it('chain with resolved object where member is not a function returns undefined', async () => {
      // getText returns a plain object { name: 'test' }, then chain accesses 'name' (not a function)
      const evaluateMock = vi.fn().mockResolvedValue(resultOk({ name: 'test' }));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));
      const getResult = chainCall(proxy, 'getData');
      const result = await (chainCall(getResult, 'name') as Promise<unknown>);

      // 'name' is 'test' (a string), not a function, so should return undefined
      expect(result).toBeUndefined();
    });
  });

  // ── getAggregation forwarder ──────────────────────────────────────
  describe('getAggregation forwarding', () => {
    it('getAggregation returns cached forwarder', () => {
      const proxy = createControlProxy(createTestState());
      // eslint-disable-next-line @typescript-eslint/unbound-method -- testing proxy returns same cached forwarder reference
      const fn1 = proxy.getAggregation;
      // eslint-disable-next-line @typescript-eslint/unbound-method -- testing proxy returns same cached forwarder reference
      const fn2 = proxy.getAggregation;
      expect(fn1).toBe(fn2);
    });

    it('getAggregation calls page.evaluate with correct method name', async () => {
      const evaluateMock = vi.fn().mockResolvedValue({
        success: true,
        returnType: 'aggregation',
        uuids: ['item1'],
        objectTypes: ['sap.m.StandardListItem'],
        duration: 1,
      } satisfies MethodExecutionResult);
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = createControlProxy(createTestState({ page }));

      await proxy.getAggregation('items');

      const [, args] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(args['methodName']).toBe('getAggregation');
      expect(args['args']).toEqual(['items']);
    });
  });

  // ── Blacklist enforcement additional ──────────────────────────────
  describe('blacklist enforcement additional', () => {
    it('blacklisted error includes correct error code', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      // eslint-disable-next-line @typescript-eslint/dot-notation -- testing blacklisted property access via proxy get trap
      expect(() => proxy['constructor']).toThrow();
      try {
        // eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/no-unused-expressions -- testing blacklisted property access via proxy get trap
        proxy['constructor'];
        expect.fail('Should have thrown');
      } catch (error: unknown) {
        expect((error as Record<string, unknown>)['code']).toBe('ERR_CONTROL_METHOD');
      }
    });

    it('blacklisted error includes method name in message', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      expect(() => proxy['fireEvent']).toThrow(/fireEvent/);
    });

    it('blacklisted error includes control type and id', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      expect(() => proxy['init']).toThrow(/sap\.m\.Button#saveBtn/);
    });
  });

  // ── resolveKnownProperty default case (unknown property) ──────────
  describe('resolveKnownProperty default → dynamic forwarder', () => {
    it('unknown property name returns a function (dynamic forwarder)', () => {
      const proxy = toRecord(createControlProxy(createTestState()));
      const fn = proxy['someRandomMethod'];
      expect(typeof fn).toBe('function');
    });

    it('dynamic forwarder for unknown property calls page.evaluate', async () => {
      const evaluateMock = vi.fn().mockResolvedValue(resultOk(42));
      const page = { evaluate: evaluateMock } as unknown as Page;
      const proxy = toRecord(createControlProxy(createTestState({ page })));
      const fn = proxy['customMethod'] as () => Promise<unknown>;

      const result = await fn();

      expect(result).toBe(42);
      const [, args] = evaluateMock.mock.calls[0] as [unknown, Record<string, unknown>];
      expect(args['methodName']).toBe('customMethod');
    });
  });
});
