import { describe, expect, it, vi } from "vitest";
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
import type { ThreadActivationSnapshot } from "../../../../../src/domain/threads/activation";
import type { Thread } from "../../../../../src/domain/threads/model";
import { runtimeSnapshotForChatState } from "../../../../../src/features/chat/application/runtime/snapshot";
import { resumedThreadAction } from "../../../../../src/features/chat/application/state/actions";
import { activeThreadId } from "../../../../../src/features/chat/application/state/root-reducer";
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
import { createThreadStartActions } from "../../../../../src/features/chat/application/threads/thread-start-actions";
import { pendingWebSubmissionItem } from "../../../../../src/features/chat/application/turns/web-submission";
import { setCollaborationModeIntent } from "../../../../../src/features/chat/domain/runtime/intent";
import { deferred } from "../../../../support/async";
import { chatStateFixture, chatStateWith } from "../../support/state";

describe("thread start actions", () => {
  it("publishes newly started threads before the first turn completes", async () => {
    let state = chatStateFixture();
    const existing = threadFixture("existing");
    state = chatStateWith(state, { threadList: { listedThreads: [existing] } });
    const stateStore = createChatStateStore(state);
    const started = threadFixture("started");
    const optimistic = { ...started, preview: "first prompt" };
    const recordStartedThread = vi.fn();
    const syncThreadGoal = vi.fn();

    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread: vi.fn().mockResolvedValue(activationFixture(started)) },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread,
      syncThreadGoal,
    });

    await actions.startThread("first prompt");

    expect(stateStore.getState().threadList.listedThreads).toEqual([optimistic, existing]);
    expect(recordStartedThread).toHaveBeenCalledWith(optimistic);
    expect(syncThreadGoal).toHaveBeenCalledWith("started");
  });

  it("keeps empty-panel runtime reservations when starting the first thread", async () => {
    const stateStore = createChatStateStore(chatStateFixture());
    stateStore.dispatch({ type: "runtime/model-requested", model: "gpt-5.5" });
    stateStore.dispatch({ type: "runtime/permission-profile-requested", permissionProfile: ":workspace" });
    stateStore.dispatch({ type: "runtime/reasoning-effort-requested", effort: "high" });
    stateStore.dispatch({ type: "runtime/fast-mode-requested", fastMode: "enabled" });
    stateStore.dispatch({ type: "runtime/approvals-reviewer-requested", approvalsReviewer: "auto_review" });
    stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "plan" });
    const startThread = vi.fn().mockResolvedValue(
      activationFixture(threadFixture("started"), {
        model: "gpt-5",
        serviceTier: "fast",
      }),
    );

    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread: vi.fn(),
      syncThreadGoal: vi.fn(),
    });

    await actions.startThread("first prompt");

    expect(startThread).toHaveBeenCalledWith({
      permissions: ":workspace",
      serviceTier: "fast",
    });
    expect(stateStore.getState().runtime.active.model).toBe("gpt-5");
    expect(stateStore.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
    expect(stateStore.getState().runtime.pending.permissionProfile).toEqual({ kind: "set", value: ":workspace" });
    expect(stateStore.getState().runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
    expect(stateStore.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
    expect(stateStore.getState().runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
    expect(stateStore.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("plan"));
  });

  it("can skip newly started thread goal sync when the caller sets the first goal", async () => {
    const stateStore = createChatStateStore(chatStateFixture());
    const syncThreadGoal = vi.fn();
    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread: vi.fn().mockResolvedValue(activationFixture(threadFixture("started"))) },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread: vi.fn(),
      syncThreadGoal,
    });

    await actions.startThread("first goal", { syncGoal: false });

    expect(syncThreadGoal).not.toHaveBeenCalled();
  });

  it("retargets an explicitly preserved pending submission to the newly started thread", async () => {
    const stateStore = createChatStateStore(chatStateFixture());
    const pending = pendingWebSubmissionItem("local-web", "https://example.com", "summarize");
    if (!pending) throw new Error("Expected pending web submission");
    stateStore.dispatch({
      type: "web-submission/pending",
      submission: {
        id: pending.id,
        item: pending,
        targetThreadId: null,
        originalDraft: "/web https://example.com",
        phase: "cancellable",
      },
    });
    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread: vi.fn().mockResolvedValue(activationFixture(threadFixture("started"))) },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread: vi.fn(),
      syncThreadGoal: vi.fn(),
    });

    await actions.startThread(pending.text, { preservePendingSubmissionId: pending.id });

    expect(stateStore.getState().pendingSubmission).toMatchObject({ id: pending.id, targetThreadId: "started" });
  });

  it("does not activate a delayed new thread after its pending submission is superseded", async () => {
    const stateStore = createChatStateStore(chatStateFixture());
    const pending = pendingWebSubmissionItem("local-web", "https://example.com", "summarize");
    if (!pending) throw new Error("Expected pending web submission");
    stateStore.dispatch({
      type: "web-submission/pending",
      submission: {
        id: pending.id,
        item: pending,
        targetThreadId: null,
        originalDraft: "/web https://example.com",
        phase: "cancellable",
      },
    });
    const started = deferred<ThreadActivationSnapshot | null>();
    const recordStartedThread = vi.fn();
    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread: vi.fn(() => started.promise) },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread,
      syncThreadGoal: vi.fn(),
    });

    const starting = actions.startThread(pending.text, { preservePendingSubmissionId: pending.id });
    stateStore.dispatch(resumedThreadAction({ response: activationFixture(threadFixture("selected")) }));
    started.resolve(activationFixture(threadFixture("delayed")));

    await expect(starting).resolves.toBeNull();
    expect(activeThreadId(stateStore.getState())).toBe("selected");
    expect(recordStartedThread).not.toHaveBeenCalled();
  });

  it("starts threads with service tier from explicit effective config", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, { connection: { runtimeConfig: { ...emptyRuntimeConfigSnapshot(), serviceTier: "flex" } } });
    const stateStore = createChatStateStore(state);
    const startThread = vi.fn().mockResolvedValue(activationFixture(threadFixture("started"), { serviceTier: "flex" }));
    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread: vi.fn(),
      syncThreadGoal: vi.fn(),
    });

    await actions.startThread();

    expect(startThread).toHaveBeenCalledWith({ serviceTier: "flex" });
  });

  it("starts threads with permission profile from explicit config", async () => {
    let state = chatStateFixture();
    state = chatStateWith(state, {
      connection: {
        runtimeConfig: {
          ...emptyRuntimeConfigSnapshot(),
          startupPermissions: {
            ...emptyRuntimeConfigSnapshot().startupPermissions,
            activePermissionProfile: { id: ":workspace", extends: null },
          },
        },
      },
    });
    const stateStore = createChatStateStore(state);
    const startThread = vi.fn().mockResolvedValue(activationFixture(threadFixture("started")));
    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread: vi.fn(),
      syncThreadGoal: vi.fn(),
    });

    await actions.startThread();

    expect(startThread).toHaveBeenCalledWith({ permissions: ":workspace" });
  });

  it("keeps app-server preview when newly started threads already have one", async () => {
    const stateStore = createChatStateStore(chatStateFixture());
    const started = threadFixture("started", { preview: "server preview" });
    const recordStartedThread = vi.fn();
    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread: vi.fn().mockResolvedValue(activationFixture(started)) },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread,
      syncThreadGoal: vi.fn(),
    });

    await actions.startThread("local preview");

    expect(recordStartedThread).toHaveBeenCalledWith(started);
  });

  it("does not apply newly started threads after the transport returns no activation", async () => {
    const stateStore = createChatStateStore(chatStateFixture());
    const recordStartedThread = vi.fn();
    const syncThreadGoal = vi.fn();
    const actions = createThreadStartActions({
      stateStore,
      threadStartTransport: { startThread: vi.fn().mockResolvedValue(null) },
      runtimeSnapshotForState: runtimeSnapshotForChatState,
      recordStartedThread,
      syncThreadGoal,
    });

    await expect(actions.startThread("local preview")).resolves.toBeNull();
    expect(activeThreadId(stateStore.getState())).toBeNull();
    expect(stateStore.getState().threadList.listedThreads).toEqual([]);
    expect(recordStartedThread).not.toHaveBeenCalled();
    expect(syncThreadGoal).not.toHaveBeenCalled();
  });
});

function threadFixture(id: string, overrides: Partial<Thread> = {}): Thread {
  return {
    id,
    preview: "",
    name: null,
    archived: false,
    provenance: { kind: "interactive" },
    createdAt: 0,
    updatedAt: 0,
    recencyAt: null,
    ...overrides,
  };
}

function activationFixture(thread: Thread, overrides: Partial<ThreadActivationSnapshot> = {}): ThreadActivationSnapshot {
  return {
    thread,
    cwd: "/vault",
    model: "gpt-5",
    serviceTier: null,
    approvalsReviewer: null,
    reasoningEffort: null,
    approvalPolicy: null,
    sandboxPolicy: null,
    activePermissionProfile: null,
    approvalPolicyKnown: false,
    sandboxPolicyKnown: false,
    permissionProfileKnown: false,
    ...overrides,
  };
}
