// @vitest-environment jsdom

import { FileSystemAdapter } from "obsidian";
import { vi } from "vitest";

import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_THREADS } from "../../src/constants";
import type { Thread } from "../../src/domain/threads/model";
import { createThreadGoalOperationCoordinator } from "../../src/features/chat/application/threads/goal-actions";
import type { CodexChatHost } from "../../src/features/chat/host/contracts";
import type { CodexChatView } from "../../src/features/chat/host/view.obsidian";
import { createThreadNameMutationCoordinator } from "../../src/features/threads/workflows/thread-name-mutation-coordinator";
import type CodexPanelPlugin from "../../src/main";
import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../src/settings/model";
import { createKeyedOperationQueue } from "../../src/shared/runtime/keyed-operation-queue";
import { chatPanelSettingsAccess } from "../features/chat/support/settings";

export async function pluginWithLeaves(leaves: TestLeaf[], options: { threadsLeaves?: TestLeaf[] } = {}): Promise<CodexPanelPlugin> {
  const { default: CodexPanelPlugin } = await import("../../src/main");
  const adapter = new FileSystemAdapter();
  vi.spyOn(adapter, "getBasePath").mockReturnValue("/vault");
  const plugin = new CodexPanelPlugin(
    {
      vault: { adapter },
      workspace: {
        getLeavesOfType: vi.fn((type: string) => {
          if (type === VIEW_TYPE_CODEX_PANEL) return leaves;
          if (type === VIEW_TYPE_CODEX_THREADS) return options.threadsLeaves ?? [];
          return [];
        }),
        revealLeaf: vi.fn().mockResolvedValue(undefined),
        getRightLeaf: vi.fn(() => null),
        createLeafInParent: vi.fn(() => null),
        getMostRecentLeaf: vi.fn(() => null),
        getActiveViewOfType: vi.fn(() => null),
        ensureSideLeaf: vi.fn(() => Promise.reject(new Error("Unexpected ensureSideLeaf call."))),
        on: vi.fn(() => ({})),
        activeLeaf: null,
        rightSplit: {},
      },
    } as never,
    {} as never,
  );
  plugin.settings = { ...DEFAULT_SETTINGS };
  plugin.vaultPath = "/vault";
  plugin.runtime.initialize();
  return plugin;
}

export async function publishCodexPath(plugin: CodexPanelPlugin, codexPath: string): Promise<void> {
  await plugin.runtime.settingTabHost().publishSettings({ ...plugin.settings, codexPath });
}

export interface TestLeaf {
  view: unknown;
  getViewState: ReturnType<typeof vi.fn>;
  getRoot: ReturnType<typeof vi.fn>;
  parent: object;
  setViewState: ReturnType<typeof vi.fn>;
  loadIfDeferred: ReturnType<typeof vi.fn>;
  detach: ReturnType<typeof vi.fn>;
}

export function leaf(options: { state?: Record<string, unknown> } = {}): TestLeaf {
  return {
    view: null,
    getViewState: vi.fn(() => ({ type: VIEW_TYPE_CODEX_PANEL, state: options.state ?? {} })),
    getRoot: vi.fn(() => ({})),
    parent: {},
    setViewState: vi.fn().mockResolvedValue(undefined),
    loadIfDeferred: vi.fn().mockResolvedValue(undefined),
    detach: vi.fn(),
  };
}

export function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf): CodexChatView {
  const containerEl = document.createElement("div");
  containerEl.createDiv();
  containerEl.createDiv();
  const view = new CodexChatViewCtor(
    {
      ...leaf,
      app: {
        workspace: {
          getActiveFile: vi.fn(() => null),
          getLastOpenFiles: vi.fn(() => []),
          on: vi.fn(() => ({})),
          openLinkText: vi.fn(),
          requestSaveLayout: vi.fn(),
        },
        vault: {
          on: vi.fn(() => ({})),
          offref: vi.fn(),
          getFiles: vi.fn(() => []),
          getMarkdownFiles: vi.fn(() => []),
          getAbstractFileByPath: vi.fn(() => null),
        },
        metadataCache: {
          on: vi.fn(() => ({})),
          offref: vi.fn(),
          getFirstLinkpathDest: vi.fn(() => null),
          fileToLinktext: vi.fn((_file: unknown, _sourcePath: string) => ""),
          getFileCache: vi.fn(() => null),
        },
      },
      containerEl,
    } as never,
    {
      attachChatView: (runtimeView) => {
        runtimeView.attachRuntime(chatHostFixture());
        runtimeView.activateRuntime();
      },
      detachChatView: (runtimeView) => {
        runtimeView.detachRuntime();
      },
    },
  );
  const workspace = view.app.workspace as unknown as {
    getActiveViewOfType?: ReturnType<typeof vi.fn>;
  };
  workspace.getActiveViewOfType ??= vi.fn();
  workspace.getActiveViewOfType.mockReturnValue(view);
  return view;
}

function chatHostFixture(): CodexChatHost {
  const settings: CodexPanelSettings = { ...DEFAULT_SETTINGS, codexPath: "codex", sendShortcut: "enter" };
  return {
    appServerClientAccess: {
      withClient: vi.fn(() => Promise.reject(new Error("Unexpected app-server client request."))),
    },
    appServerContext: { codexPath: settings.codexPath, vaultPath: "/vault" },
    threadNameMutations: createThreadNameMutationCoordinator(),
    threadTitleTransport: {
      persistedContext: vi.fn().mockResolvedValue(null),
      generateTitle: vi.fn().mockResolvedValue(null),
    },
    threadGoalOperations: createThreadGoalOperationCoordinator(),
    runtimeSettingsCommitQueue: createKeyedOperationQueue(),
    settingsRef: {
      settings: chatPanelSettingsAccess(settings),
      vaultPath: "/vault",
    },
    workspace: {
      openThreadInNewView: vi.fn(),
      openThreadFromPanel: vi.fn(),
      focusThreadInOpenView: vi.fn(),
      threadHasPendingOrRunningPanel: vi.fn(() => false),
      openTurnDiff: vi.fn(),
      notifyPanelActivityChanged: vi.fn(),
      openSideChat: vi.fn(),
    },
    appServerQueries: {
      appServerMetadataSnapshot: vi.fn(() => null),
      refreshAppServerMetadata: vi.fn(() => Promise.resolve()),
      refreshSkills: vi.fn(() => Promise.resolve()),
      refreshRateLimits: vi.fn(() => Promise.resolve()),
      modelsSnapshot: vi.fn(() => null),
      fetchModels: vi.fn(() => Promise.resolve([])),
      refreshModels: vi.fn(() => Promise.resolve([])),
      observeAppServerMetadataResources: vi.fn(() => () => undefined),
      observeModelsResult: vi.fn(() => () => undefined),
    },
    threadCatalog: {
      apply: vi.fn(),
      loadActive: vi.fn(() => Promise.resolve([])),
      refreshActive: vi.fn(() => Promise.resolve([])),
      activeSnapshot: vi.fn(() => null),
      recentActiveSnapshot: vi.fn(() => null),
      hasMoreActive: vi.fn(() => false),
      loadMoreActive: vi.fn(() => Promise.resolve([])),
      observeActive: vi.fn(() => () => undefined),
    },
  };
}

export function threadListClient(fetchThreads: () => Promise<readonly Thread[]>): never {
  return {
    request: async (method: string) => {
      if (method !== "thread/list") throw new Error(`Unexpected app-server request: ${method}`);
      return { data: await fetchThreads(), nextCursor: null };
    },
  } as never;
}

export async function flushMicrotasks(): Promise<void> {
  await new Promise((resolve) => setImmediate(resolve));
}

export function thread(id: string): Thread {
  return {
    id,
    preview: id,
    createdAt: 1,
    updatedAt: 1,
    name: null,
    archived: false,
    provenance: { kind: "interactive" },
  };
}

export function panelSnapshot(overrides: PanelSnapshotFixtureOverrides = {}): ReturnType<CodexChatView["surface"]["openPanelSnapshot"]> {
  const { pendingApprovals, pendingUserInputs, pendingMcpElicitations, ...snapshotOverrides } = overrides;
  return {
    viewId: "view",
    threadId: "thread",
    turnBusy: false,
    pending: (pendingApprovals ?? 0) + (pendingUserInputs ?? 0) + (pendingMcpElicitations ?? 0) > 0,
    hasComposerDraft: false,
    connected: true,
    ...snapshotOverrides,
  };
}

type PanelSnapshotFixtureOverrides = Partial<ReturnType<CodexChatView["surface"]["openPanelSnapshot"]>> & {
  pendingApprovals?: number;
  pendingUserInputs?: number;
  pendingMcpElicitations?: number;
};
