import { describe, expect, it, vi } from "vitest";
import type { ModelMetadata } from "../../../../../src/domain/catalog/metadata";
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
import {
  type ChatRuntimeSettingsActions,
  createChatRuntimeSettingsActions,
} from "../../../../../src/features/chat/application/runtime/settings-actions";
import type { RuntimeSettingsTransport } from "../../../../../src/features/chat/application/runtime/settings-transport";
import { runtimeSnapshotForChatState } from "../../../../../src/features/chat/application/runtime/snapshot";
import type { ActiveThreadSettingsAppliedAction } from "../../../../../src/features/chat/application/state/actions";
import { activeThreadId, type ChatState } from "../../../../../src/features/chat/application/state/root-reducer";
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
import { setCollaborationModeIntent, setRuntimeIntentValue } from "../../../../../src/features/chat/domain/runtime/intent";
import { chatStateFixture, chatStateWith } from "../../support/state";

describe("createChatRuntimeSettingsActions", () => {
  it("applies pending runtime intents through thread settings and commits them", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = createChatRuntimeSettingsActions({
      stateStore: store,
      runtimeTransport: transport,
      runtimeSnapshotForState: runtimeSnapshotFixture,
      collaborationModeLabel: () => "Plan",
      addSystemMessage: (text) => messages.push(text),
    });

    await expect(actions.requestModel("gpt-5.5")).resolves.toBe(true);

    expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
    expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
    expect(store.getState().runtime.active.model).toBe("gpt-5.5");
    expect(messages).toEqual([]);
  });

  it("commits pending permission profile updates into active permission state", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, {
      runtime: {
        active: {
          activePermissionProfile: { id: ":read-only", extends: null },
          sandboxPolicy: { type: "readOnly", networkAccess: false },
        },
        pending: {
          permissionProfile: setRuntimeIntentValue(":workspace"),
        },
      },
    });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.applyPendingThreadSettings()).resolves.toBe(true);

    expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { permissions: ":workspace" });
    expect(store.getState().runtime.pending.permissionProfile).toEqual({ kind: "unchanged" });
    expect(store.getState().runtime.active.activePermissionProfile).toEqual({ id: ":workspace", extends: null });
    expect(store.getState().runtime.active.permissionProfileKnown).toBe(true);
    expect(store.getState().runtime.active.sandboxPolicy).toBeNull();
    expect(store.getState().runtime.active.sandboxPolicyKnown).toBe(true);
    expect(messages).toEqual([]);
  });

  it("requests and resets permission profiles through thread settings", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, {
      runtime: {
        active: {
          activePermissionProfile: { id: ":read-only", extends: null },
          sandboxPolicy: { type: "readOnly", networkAccess: false },
        },
      },
    });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestPermissionProfile(":workspace")).resolves.toBe(true);
    await expect(actions.resetPermissionProfileToConfig()).resolves.toBe(true);

    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { permissions: ":workspace" });
    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { permissions: null });
    expect(store.getState().runtime.pending.permissionProfile).toEqual({ kind: "unchanged" });
    expect(store.getState().runtime.active.activePermissionProfile).toBeNull();
    expect(store.getState().runtime.active.permissionProfileKnown).toBe(false);
    expect(messages).toEqual([]);
  });

  it("blocks permission profile changes for ephemeral side chats", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, {
      activeThread: {
        id: "side",
        lifetime: { kind: "ephemeral", sourceThreadId: "source", sourceThreadTitle: "Source" },
      },
    });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestPermissionProfile(":workspace")).resolves.toBe(false);
    await expect(actions.resetPermissionProfileToConfig()).resolves.toBe(false);

    expect(transport.updateThreadSettings).not.toHaveBeenCalled();
    expect(store.getState().runtime.pending.permissionProfile).toEqual({ kind: "unchanged" });
    expect(messages).toEqual(["Permission changes are unavailable in side chats.", "Permission changes are unavailable in side chats."]);
  });

  it("blocks all thread setting changes for subagent threads", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, {
      activeThread: {
        id: "child",
        provenance: {
          kind: "subagent",
          subagentKind: "thread-spawn",
          parentThreadId: "parent",
          sessionId: "session",
          depth: 1,
          agentNickname: null,
          agentRole: null,
        },
      },
    });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);
    await expect(actions.requestReasoningEffort("high")).resolves.toBe(false);
    await expect(actions.requestPermissionProfile(":workspace")).resolves.toBe(false);
    await expect(actions.setCollaborationMode("plan")).resolves.toBe(false);
    await actions.enableFastMode();
    await actions.enableAutoReview();

    expect(transport.updateThreadSettings).not.toHaveBeenCalled();
    expect(messages).toHaveLength(6);
    expect(new Set(messages)).toEqual(new Set(["Thread settings are unavailable in agent threads."]));
  });

  it("reserves thread runtime settings when no thread is active", async () => {
    const store = createChatStateStore(chatStateFixture());
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestModel("gpt-5.5")).resolves.toBe(true);
    await expect(actions.requestPermissionProfile(":workspace")).resolves.toBe(true);
    await expect(actions.requestReasoningEffort("high")).resolves.toBe(true);
    await actions.enableFastMode();
    await actions.enableAutoReview();
    await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);
    actions.requestDefaultCollaborationModeForNextTurn();

    expect(transport.updateThreadSettings).not.toHaveBeenCalled();
    expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
    expect(store.getState().runtime.pending.permissionProfile).toEqual({ kind: "set", value: ":workspace" });
    expect(store.getState().runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
    expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
    expect(store.getState().runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
    expect(store.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("default"));
    expect(messages).toEqual([
      "Fast mode on for subsequent turns.",
      "Auto-review on for subsequent turns.",
      "Plan mode on for subsequent turns.",
    ]);
  });

  it("toggles fast mode and reports the user-visible result", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, { ui: { toolbarPanel: "status-panel" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await actions.toggleFastMode();

    expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { serviceTier: "fast" });
    expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "unchanged" });
    expect(store.getState().runtime.active.serviceTier).toBe("fast");
    expect(store.getState().ui.toolbarPanel).toBeNull();
    expect(messages).toEqual(["Fast mode on for subsequent turns."]);
  });

  it("enables and disables fast mode through explicit commands", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await actions.enableFastMode();
    store.dispatch({ type: "active-thread/settings-applied", ...threadSettings("fast") });
    await actions.disableFastMode();

    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { serviceTier: "fast" });
    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { serviceTier: null });
    expect(messages).toEqual(["Fast mode on for subsequent turns.", "Fast mode off for subsequent turns."]);
  });

  it("keeps Fast disabled after clearing a thread tier when config defaults to Fast", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, { connection: { runtimeConfig: { ...emptyRuntimeConfigSnapshot(), serviceTier: "fast" } } });
    state = chatStateWith(state, { runtime: { active: { serviceTier: "fast" } } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await actions.disableFastMode();

    expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: null });
    expect(store.getState().runtime.active.serviceTier).toBeNull();
    expect(store.getState().runtime.active.serviceTierKnown).toBe(true);

    await actions.toggleFastMode();

    expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: "fast" });
    expect(messages).toEqual(["Fast mode off for subsequent turns.", "Fast mode on for subsequent turns."]);
  });

  it("requests the catalog Fast tier id and toggles it off from the reported effective id", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, { runtime: { active: { model: "gpt-5.5" } } });
    // app-server may advertise Fast with an id such as "priority";
    // last verified against codex app-server 0.142.0.
    state = chatStateWith(state, { connection: { availableModels: [modelFixture("gpt-5.5", "priority")] } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await actions.toggleFastMode();

    expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: "priority" });
    expect(store.getState().runtime.active.serviceTier).toBe("priority");

    store.dispatch({ type: "active-thread/settings-applied", ...threadSettings("priority") });
    await actions.toggleFastMode();

    expect(transport.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: null });
    expect(store.getState().runtime.active.serviceTier).toBeNull();
    expect(messages).toEqual(["Fast mode on for subsequent turns.", "Fast mode off for subsequent turns."]);
  });

  it("warns without reporting collaboration mode as applied when no effective model is available", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);

    expect(transport.updateThreadSettings).not.toHaveBeenCalled();
    expect(store.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("plan"));
    expect(store.getState().runtime.active.collaborationMode).toBeNull();
    expect(messages).toEqual(["Plan mode is selected, but No effective model is available. Sending without a mode override."]);
  });

  it("requests default collaboration mode for the next turn without applying thread settings", () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, { runtime: { active: { collaborationMode: "plan" } } });
    state = chatStateWith(state, { runtime: { pending: { collaborationMode: setCollaborationModeIntent("plan") } } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    actions.requestDefaultCollaborationModeForNextTurn();

    expect(transport.updateThreadSettings).not.toHaveBeenCalled();
    expect(store.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("default"));
    expect(store.getState().runtime.active.collaborationMode).toBe("plan");
    expect(messages).toEqual([]);
  });

  it("builds pending thread settings from explicit effective config", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, {
      connection: {
        runtimeConfig: {
          ...emptyRuntimeConfigSnapshot(),
          model: "gpt-config",
          reasoningEffort: "medium",
        },
      },
    });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = createChatRuntimeSettingsActions({
      stateStore: store,
      runtimeTransport: transport,
      runtimeSnapshotForState: (state) => ({ ...runtimeSnapshotFixture(state), runtimeConfig: null }),
      collaborationModeLabel: () => "Plan",
      addSystemMessage: (text) => messages.push(text),
    });

    await expect(actions.setCollaborationMode("plan")).resolves.toBe(true);

    expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", {
      collaborationMode: {
        mode: "plan",
        settings: {
          model: "gpt-config",
          reasoningEffort: "medium",
          developerInstructions: null,
        },
      },
    });
    expect(messages).toEqual(["Plan mode on for subsequent turns."]);
  });

  it("leaves pending runtime intent in place when the app-server update fails", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockRejectedValue(new Error("nope")) });
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);

    expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
    expect(store.getState().runtime.active.model).toBeNull();
    expect(messages).toEqual(["nope"]);
  });

  it("leaves pending runtime intent in place when the runtime transport is unavailable", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockResolvedValue(false) });
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);

    expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
    expect(store.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
    expect(store.getState().runtime.active.model).toBeNull();
    expect(messages).toEqual([]);
  });

  it("keeps the runtime panel open when a toolbar runtime update fails", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, { ui: { toolbarPanel: "status-panel" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture({ updateThreadSettings: vi.fn().mockRejectedValue(new Error("nope")) });
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await actions.enableFastMode();

    expect(store.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
    expect(store.getState().ui.toolbarPanel).toBe("status-panel");
    expect(messages).toEqual(["nope"]);
  });

  it("does not commit stale runtime updates after the active thread changes", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture({
      updateThreadSettings: vi.fn().mockImplementation(async () => {
        store.dispatch({ type: "active-thread/cleared" });
        return true;
      }),
    });
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);

    expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: "gpt-5.5" });
    expect(activeThreadId(store.getState())).toBeNull();
    expect(store.getState().runtime.active.model).toBeNull();
    expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
    expect(messages).toEqual([]);
  });

  it("does not report stale runtime update failures after the active thread changes", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture({
      updateThreadSettings: vi.fn().mockImplementation(async () => {
        store.dispatch({ type: "active-thread/cleared" });
        throw new Error("nope");
      }),
    });
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.requestModel("gpt-5.5")).resolves.toBe(false);

    expect(activeThreadId(store.getState())).toBeNull();
    expect(store.getState().runtime.active.model).toBeNull();
    expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
    expect(messages).toEqual([]);
  });

  it("does not commit stale runtime updates after a newer pending intent replaces them", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const firstUpdate = deferred(true);
    const secondUpdate = deferred(true);
    const transport = settingsTransportFixture({
      updateThreadSettings: vi
        .fn()
        .mockImplementationOnce(() => firstUpdate.promise)
        .mockImplementationOnce(() => secondUpdate.promise),
    });
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    const firstRequest = actions.requestModel("gpt-old");
    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { model: "gpt-old" });

    const secondRequest = actions.requestModel("gpt-new");
    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { model: "gpt-new" });

    secondUpdate.resolve();
    await expect(secondRequest).resolves.toBe(true);
    expect(store.getState().runtime.active.model).toBe("gpt-new");
    expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });

    firstUpdate.resolve();
    await expect(firstRequest).resolves.toBe(false);
    expect(store.getState().runtime.active.model).toBe("gpt-new");
    expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
    expect(messages).toEqual([]);
  });

  it("resets requested model to config through an explicit command", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    state = chatStateWith(state, { runtime: { active: { model: "gpt-5.5" } } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await expect(actions.resetModelToConfig()).resolves.toBe(true);

    expect(transport.updateThreadSettings).toHaveBeenCalledWith("thread", { model: null });
    expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
    expect(store.getState().runtime.active.model).toBeNull();
  });

  it("enables and disables auto-review through explicit commands", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { activeThread: { id: "thread" } });
    const store = createChatStateStore(state);
    const transport = settingsTransportFixture();
    const messages: string[] = [];
    const actions = runtimeActionsFixture(store, transport, messages);

    await actions.enableAutoReview();
    store.dispatch({ type: "active-thread/settings-applied", ...threadSettings(null, "auto_review") });
    await actions.disableAutoReview();

    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { approvalsReviewer: "auto_review" });
    expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { approvalsReviewer: "user" });
    expect(messages).toEqual(["Auto-review on for subsequent turns.", "Auto-review off for subsequent turns."]);
  });
});

function runtimeActionsFixture(
  store: ReturnType<typeof createChatStateStore>,
  transport: RuntimeSettingsTransport,
  messages: string[],
): ChatRuntimeSettingsActions {
  return createChatRuntimeSettingsActions({
    stateStore: store,
    runtimeTransport: transport,
    runtimeSnapshotForState: runtimeSnapshotFixture,
    collaborationModeLabel: () => "Plan",
    addSystemMessage: (text) => messages.push(text),
  });
}

function settingsTransportFixture(overrides: Partial<RuntimeSettingsTransport> = {}): RuntimeSettingsTransport {
  return {
    updateThreadSettings: vi.fn().mockResolvedValue(true),
    ...overrides,
  };
}

function runtimeSnapshotFixture(state: ChatState) {
  return runtimeSnapshotForChatState(state);
}

function modelFixture(model: string, fastTierId: string): ModelMetadata {
  return {
    id: model,
    model,
    displayName: model,
    description: "",
    hidden: false,
    supportedReasoningEfforts: [],
    defaultReasoningEffort: "medium",
    inputModalities: [],
    serviceTiers: [{ id: fastTierId, name: "Fast" }],
    defaultServiceTier: null,
    isDefault: true,
  };
}

function threadSettings(
  serviceTier: string | null,
  approvalsReviewer: Omit<ActiveThreadSettingsAppliedAction, "type">["approvalsReviewer"] = "user",
): Omit<ActiveThreadSettingsAppliedAction, "type"> {
  return {
    cwd: "/vault",
    model: "gpt-5.5",
    reasoningEffort: "high",
    collaborationMode: "default",
    serviceTier,
    approvalsReviewer,
    approvalPolicyKnown: true,
    sandboxPolicyKnown: true,
    permissionProfileKnown: true,
    approvalPolicy: null,
    sandboxPolicy: null,
    activePermissionProfile: null,
  };
}

function deferred<T>(initialValue: T): { promise: Promise<T>; resolve: (value?: T) => void } {
  let resolve!: (value: T) => void;
  const promise = new Promise<T>((promiseResolve) => {
    resolve = promiseResolve;
  });
  return {
    promise,
    resolve: (value = initialValue) => {
      resolve(value);
    },
  };
}
