/**
 * Unit tests for `isTauri()` — the canonical Tauri-runtime guard used across
 * `app/src/`. Beyond delegating to `@tauri-apps/api/core::isTauri()`, this
 * wrapper also confirms that the IPC transport (`window.__TAURI_INTERNALS__
 * .invoke`) is wired before reporting `true`.
 *
 * Why it matters: under CEF, `globalThis.isTauri` (which the underlying
 * `coreIsTauri()` checks) is injected by the webview bootstrap BEFORE the
 * `postMessage` IPC bridge is connected. An `invoke()` landing in that gap
 * throws `TypeError: Cannot read properties of undefined (reading
 * 'postMessage')` deep inside Tauri's `sendIpcMessage`, which surfaces as
 * the OPENHUMAN-REACT-S Sentry issue (#1472 follow-up). All call sites that
 * gate on `isTauri()` should now route through the non-Tauri branch during
 * the gap instead of bursting into IPC.
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { isTauri, parseServiceCliOutput } from './common';

const coreIsTauriMock = vi.fn();

vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => coreIsTauriMock() }));

describe('isTauri (tauriCommands/common)', () => {
  // We mutate `window` to simulate Tauri-runtime bootstrap state across cases.
  // Stash + restore so other tests in the suite (which share the jsdom global)
  // see a pristine window.
  let originalInternals: unknown;

  beforeEach(() => {
    coreIsTauriMock.mockReset();
    originalInternals = (window as unknown as { __TAURI_INTERNALS__?: unknown })
      .__TAURI_INTERNALS__;
  });

  afterEach(() => {
    if (originalInternals === undefined) {
      delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
    } else {
      (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ =
        originalInternals;
    }
  });

  it('returns false when not running in Tauri at all (browser/Vitest)', () => {
    coreIsTauriMock.mockReturnValue(false);
    delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;

    expect(isTauri()).toBe(false);
  });

  it('returns true when both the runtime flag and the IPC `invoke` handle are present', () => {
    coreIsTauriMock.mockReturnValue(true);
    (window as unknown as { __TAURI_INTERNALS__?: { invoke: unknown } }).__TAURI_INTERNALS__ = {
      invoke: () => Promise.resolve(),
    };

    expect(isTauri()).toBe(true);
  });

  // The OPENHUMAN-REACT-S regression: Tauri sets `globalThis.isTauri = true`
  // (so the official check returns true) before CEF wires the IPC postMessage
  // bridge. During that gap any unguarded `invoke(...)` blows up inside
  // `sendIpcMessage` with the "Cannot read properties of undefined (reading
  // 'postMessage')" TypeError. Our guard must short-circuit to `false` so
  // call sites skip the IPC path instead of trusting the runtime flag alone.
  it('returns false during the CEF gap when runtime flag is set but __TAURI_INTERNALS__ is missing', () => {
    coreIsTauriMock.mockReturnValue(true);
    delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;

    expect(isTauri()).toBe(false);
  });

  it('returns false during the partial-bootstrap gap when __TAURI_INTERNALS__ exists but `invoke` is not yet wired', () => {
    coreIsTauriMock.mockReturnValue(true);
    // Some CEF bootstrap stages set the object literal before the IPC handle
    // is attached. Treat that as "not ready".
    (window as unknown as { __TAURI_INTERNALS__?: Record<string, unknown> }).__TAURI_INTERNALS__ =
      {};

    expect(isTauri()).toBe(false);
  });

  it('returns false when __TAURI_INTERNALS__.invoke is present but not a function', () => {
    coreIsTauriMock.mockReturnValue(true);
    (window as unknown as { __TAURI_INTERNALS__?: { invoke: unknown } }).__TAURI_INTERNALS__ = {
      invoke: 'not-a-function',
    };

    expect(isTauri()).toBe(false);
  });
});

// `parseServiceCliOutput` runs against raw CLI stdout from the `openhuman`
// sidecar. The core process can crash mid-write, return a partial response, or
// drift from the expected JSON schema across versions — any of which produces
// malformed input. The guard must reject those cases with a descriptive error
// rather than handing typed garbage back to callers.
describe('parseServiceCliOutput (tauriCommands/common)', () => {
  it('returns the parsed response when shape matches CommandResponse', () => {
    const raw = JSON.stringify({ result: { value: 42 }, logs: ['ok'] });

    const parsed = parseServiceCliOutput<{ value: number }>(raw);

    expect(parsed.result).toEqual({ value: 42 });
    expect(parsed.logs).toEqual(['ok']);
  });

  it('accepts null result and empty logs (valid CommandResponse shape)', () => {
    const raw = JSON.stringify({ result: null, logs: [] });

    const parsed = parseServiceCliOutput<null>(raw);

    expect(parsed.result).toBeNull();
    expect(parsed.logs).toEqual([]);
  });

  it('throws a descriptive error when the input is not valid JSON', () => {
    expect(() => parseServiceCliOutput('not-json')).toThrow(/Failed to parse service CLI output/);
  });

  it('throws when the parsed value is null', () => {
    expect(() => parseServiceCliOutput('null')).toThrow(/CommandResponse shape/);
  });

  it('throws when the parsed value is an array (not an object)', () => {
    expect(() => parseServiceCliOutput('[]')).toThrow(/CommandResponse shape/);
  });

  it('throws when required `logs` field is missing', () => {
    expect(() => parseServiceCliOutput(JSON.stringify({ result: 1 }))).toThrow(
      /CommandResponse shape/
    );
  });

  it('throws when required `result` field is missing', () => {
    expect(() => parseServiceCliOutput(JSON.stringify({ logs: [] }))).toThrow(
      /CommandResponse shape/
    );
  });

  it('throws when `logs` is not an array', () => {
    expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: 'oops' }))).toThrow(
      /CommandResponse shape/
    );
  });

  it('throws when `logs` contains non-string entries', () => {
    expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: [1, 2] }))).toThrow(
      /CommandResponse shape/
    );
  });
});
