// Tests for playback mode store
// TRACES: UR-010 | DR-037
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { get } from "svelte/store";

// Mock Tauri invoke
const mockInvoke = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
  invoke: (...args: unknown[]) => mockInvoke(...args),
}));

// Mock the sessions store. `selectedSession` is read via svelte's `get()`, which
// calls subscribe and synchronously receives the current value; tests set
// `currentSelectedSession` to control what the store yields.
const mockSelectSession = vi.fn();
let currentSelectedSession: unknown = null;
vi.mock("./sessions", () => ({
  sessions: {
    selectSession: (...args: unknown[]) => mockSelectSession(...args),
  },
  selectedSession: {
    subscribe: vi.fn((callback: (value: unknown) => void) => {
      callback(currentSelectedSession);
      return () => {};
    }),
  },
}));

// Capture the playerStatusEvent listener so tests can drive backend
// `playback_mode_changed` events through the reconciler. The commands still flow
// to the real bindings (which call the mocked `invoke`), so the existing
// refresh/transfer tests keep exercising the true command path.
let capturedStatusListener: ((event: { payload: any }) => void) | null = null;
vi.mock("@tauri-apps/api/event", () => ({
  listen: vi.fn((_name: string, cb: (event: { payload: any }) => void) => {
    capturedStatusListener = cb;
    return Promise.resolve(() => {});
  }),
}));

// Mock auth store
const mockGetHandle = vi.fn(() => "repo-handle-1");
vi.mock("./auth", () => ({
  auth: {
    getRepository: vi.fn(() => ({
      getPlaybackInfo: vi.fn().mockResolvedValue({ streamUrl: "http://test.com/stream" }),
      getImageUrl: vi.fn().mockReturnValue("http://test.com/image"),
      getHandle: () => mockGetHandle(),
    })),
  },
}));

describe("playbackMode store", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    currentSelectedSession = null;
    capturedStatusListener = null;
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe("initial state", () => {
    it("should have idle mode initially", async () => {
      const { playbackMode } = await import("./playbackMode");
      const state = get(playbackMode);

      expect(state.mode).toBe("idle");
      expect(state.remoteSessionId).toBeNull();
      expect(state.isTransferring).toBe(false);
      expect(state.transferError).toBeNull();
    });
  });

  describe("setMode", () => {
    it("should update mode locally", async () => {
      const { playbackMode } = await import("./playbackMode");

      playbackMode.setMode("local");
      let state = get(playbackMode);
      expect(state.mode).toBe("local");

      playbackMode.setMode("remote", "session-123");
      state = get(playbackMode);
      expect(state.mode).toBe("remote");
      expect(state.remoteSessionId).toBe("session-123");
    });
  });

  describe("disconnect", () => {
    it("should notify Rust backend and update local state when disconnecting", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Set up remote mode first
      playbackMode.setMode("remote", "session-123");
      let state = get(playbackMode);
      expect(state.mode).toBe("remote");
      expect(state.remoteSessionId).toBe("session-123");

      // Mock successful Rust call
      mockInvoke.mockResolvedValueOnce(undefined);

      // Call disconnect
      await playbackMode.disconnect();

      // Verify Rust backend was notified with correct mode (mode is now an object with type field)
      expect(mockInvoke).toHaveBeenCalledWith("playback_mode_set", {
        mode: { type: "idle" },
      });

      // Verify sessions.selectSession was called with null
      expect(mockSelectSession).toHaveBeenCalledWith(null);

      // Verify local state updated
      state = get(playbackMode);
      expect(state.mode).toBe("idle");
      expect(state.remoteSessionId).toBeNull();
      expect(state.transferError).toBeNull();
    });

    it("should do nothing if not in remote mode", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Ensure we're in idle mode
      playbackMode.setMode("idle");
      const state = get(playbackMode);
      expect(state.mode).toBe("idle");

      // Call disconnect
      await playbackMode.disconnect();

      // Verify Rust backend was NOT called
      expect(mockInvoke).not.toHaveBeenCalled();
      expect(mockSelectSession).not.toHaveBeenCalled();
    });

    it("should handle errors gracefully", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Set up remote mode first
      playbackMode.setMode("remote", "session-123");

      // Mock failed Rust call
      const error = new Error("Failed to set mode");
      mockInvoke.mockRejectedValueOnce(error);

      // Call disconnect and expect it to throw
      await expect(playbackMode.disconnect()).rejects.toThrow("Failed to set mode");

      // Verify error was stored in state
      const state = get(playbackMode);
      expect(state.transferError).toBe("Failed to set mode");
    });

    it("should clear previous transfer errors on successful disconnect", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Set up remote mode with an existing error
      playbackMode.setMode("remote", "session-123");

      // Manually set an error state (simulating a previous failed operation)
      // We'll use clearError then verify it's cleared on disconnect
      mockInvoke.mockResolvedValueOnce(undefined);

      await playbackMode.disconnect();

      const state = get(playbackMode);
      expect(state.transferError).toBeNull();
    });
  });

  describe("transferToRemote", () => {
    it("should call Rust backend with session ID", async () => {
      const { playbackMode } = await import("./playbackMode");

      mockInvoke.mockResolvedValueOnce(undefined);

      await playbackMode.transferToRemote("session-456");

      expect(mockInvoke).toHaveBeenCalledWith("playback_mode_transfer_to_remote", {
        sessionId: "session-456",
        position: null,
      });
    });

    it("should update local state on success", async () => {
      const { playbackMode } = await import("./playbackMode");

      mockInvoke.mockResolvedValueOnce(undefined);

      await playbackMode.transferToRemote("session-456");

      const state = get(playbackMode);
      expect(state.mode).toBe("remote");
      expect(state.remoteSessionId).toBe("session-456");
      expect(state.isTransferring).toBe(false);
    });

    it("should set isTransferring during transfer", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Create a promise that we can control
      let resolveTransfer: () => void;
      const transferPromise = new Promise<void>((resolve) => {
        resolveTransfer = resolve;
      });
      mockInvoke.mockReturnValueOnce(transferPromise);

      // Start the transfer (don't await)
      const transferPromiseResult = playbackMode.transferToRemote("session-789");

      // Check that isTransferring is true during the transfer
      let state = get(playbackMode);
      expect(state.isTransferring).toBe(true);

      // Resolve the transfer
      resolveTransfer!();
      await transferPromiseResult;

      // Check that isTransferring is false after
      state = get(playbackMode);
      expect(state.isTransferring).toBe(false);
    });
  });

  describe("transferToLocal", () => {
    // 50,000,000 ticks = 5 seconds (10M ticks per second).
    const REMOTE_POSITION_TICKS = 50_000_000;
    const REMOTE_POSITION_SECONDS = 5;

    function setRemoteSessionPlaying() {
      currentSelectedSession = {
        id: "session-123",
        nowPlayingItem: { id: "item-abc", name: "Test Track", runTimeTicks: 1_800_000_000 },
        playState: { positionTicks: REMOTE_POSITION_TICKS },
      };
    }

    it("resumes local playback at the remote position, not from 0", async () => {
      const { playbackMode } = await import("./playbackMode");

      playbackMode.setMode("remote", "session-123");
      setRemoteSessionPlaying();
      mockInvoke.mockResolvedValue(undefined);

      await playbackMode.transferToLocal();

      // Bug 2 guard: the resume position must be passed to play_tracks so the
      // backend seeks at load time. Restarting from 0 means startPosition is missing.
      const playTracksCall = mockInvoke.mock.calls.find((c) => c[0] === "player_play_tracks");
      expect(playTracksCall).toBeDefined();
      expect(playTracksCall![1]).toMatchObject({
        repositoryHandle: "repo-handle-1",
        request: {
          trackIds: ["item-abc"],
          startIndex: 0,
          startPosition: REMOTE_POSITION_SECONDS,
        },
      });
    });

    it("does not issue a separate player_seek (no start-at-0-then-jump race)", async () => {
      const { playbackMode } = await import("./playbackMode");

      playbackMode.setMode("remote", "session-123");
      setRemoteSessionPlaying();
      mockInvoke.mockResolvedValue(undefined);

      await playbackMode.transferToLocal();

      const seekCall = mockInvoke.mock.calls.find((c) => c[0] === "player_seek");
      expect(seekCall).toBeUndefined();
    });

    it("tells Rust to stop the remote session with the current item and position", async () => {
      const { playbackMode } = await import("./playbackMode");

      playbackMode.setMode("remote", "session-123");
      setRemoteSessionPlaying();
      mockInvoke.mockResolvedValue(undefined);

      await playbackMode.transferToLocal();

      expect(mockInvoke).toHaveBeenCalledWith("playback_mode_transfer_to_local", {
        currentItemId: "item-abc",
        positionTicks: REMOTE_POSITION_TICKS,
      });

      const state = get(playbackMode);
      expect(state.mode).toBe("local");
      expect(state.remoteSessionId).toBeNull();
      expect(state.isTransferring).toBe(false);
    });

    it("switches to local without playing media when remote has nothing playing", async () => {
      const { playbackMode } = await import("./playbackMode");

      playbackMode.setMode("remote", "session-123");
      currentSelectedSession = { id: "session-123", nowPlayingItem: null, playState: null };

      await playbackMode.transferToLocal();

      expect(mockInvoke).not.toHaveBeenCalledWith("player_play_tracks", expect.anything());
      const state = get(playbackMode);
      expect(state.mode).toBe("local");
      expect(state.remoteSessionId).toBeNull();
    });
  });

  describe("refresh (reconcile to Rust authoritative mode)", () => {
    it("adopts the Rust mode and aligns the selected session", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Start disagreeing with Rust: store thinks local, Rust says remote.
      playbackMode.setMode("local");
      mockInvoke.mockResolvedValueOnce({ type: "remote", session_id: "sess-xyz" });

      await playbackMode.refresh();

      const state = get(playbackMode);
      expect(state.mode).toBe("remote");
      expect(state.remoteSessionId).toBe("sess-xyz");
      // The merged UI stores follow selectedSession, so it must be aligned too.
      expect(mockSelectSession).toHaveBeenCalledWith("sess-xyz");
    });

    it("clears the selected session when Rust reports non-remote", async () => {
      const { playbackMode } = await import("./playbackMode");

      playbackMode.setMode("remote", "sess-old");
      mockInvoke.mockResolvedValueOnce({ type: "idle" });

      await playbackMode.refresh();

      const state = get(playbackMode);
      expect(state.mode).toBe("idle");
      expect(state.remoteSessionId).toBeNull();
      expect(mockSelectSession).toHaveBeenCalledWith(null);
    });
  });

  describe("backend playback_mode_changed reconciler", () => {
    // Regression: commit 2a1f168 made Rust re-broadcast PlaybackModeChanged on
    // every set_mode. Local playback drives set_mode("local") from both the
    // frontend and Rust, so the same mode arrives repeatedly. The reconciler
    // used to run selectSession(null) on each one, deselecting the remote
    // session mid-cast and tripping the disconnect watchdog — which broke the
    // lockscreen card, remote volume, and (via the mode flap) local audio.
    async function initListener() {
      const { playbackMode } = await import("./playbackMode");
      playbackMode.initializeSessionMonitoring();
      expect(capturedStatusListener).not.toBeNull();
      return playbackMode;
    }

    it("ignores a no-op remote re-broadcast (no session churn)", async () => {
      currentSelectedSession = { id: "sess-1" };
      const playbackMode = await initListener();
      playbackMode.setMode("remote", "sess-1");
      mockSelectSession.mockClear();

      // Rust re-broadcasts the SAME remote mode (e.g. a position tick path).
      capturedStatusListener!({
        payload: { type: "playback_mode_changed", mode: "remote", session_id: "sess-1" },
      });

      const state = get(playbackMode);
      expect(state.mode).toBe("remote");
      expect(state.remoteSessionId).toBe("sess-1");
      // Must NOT re-select (which would churn the watchdog) on a no-op.
      expect(mockSelectSession).not.toHaveBeenCalled();
    });

    it("adopts a genuine remote→local change and clears the session", async () => {
      currentSelectedSession = { id: "sess-1" };
      const playbackMode = await initListener();
      playbackMode.setMode("remote", "sess-1");
      mockSelectSession.mockClear();

      capturedStatusListener!({
        payload: { type: "playback_mode_changed", mode: "local", session_id: null },
      });

      const state = get(playbackMode);
      expect(state.mode).toBe("local");
      expect(state.remoteSessionId).toBeNull();
      expect(mockSelectSession).toHaveBeenCalledWith(null);
    });

  });

  describe("transfer reconciles to Rust on completion", () => {
    it("refreshes from Rust after a successful transferToRemote", async () => {
      const { playbackMode } = await import("./playbackMode");

      // First call: the transfer command; second call: the finally refresh.
      mockInvoke.mockResolvedValueOnce(undefined);
      mockInvoke.mockResolvedValueOnce({ type: "remote", session_id: "session-456" });

      await playbackMode.transferToRemote("session-456");

      expect(mockInvoke).toHaveBeenCalledWith("playback_mode_get_current");
    });

    it("still reconciles to Rust when transferToRemote throws mid-transfer", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Transfer command fails, leaving the optimistic state possibly wrong.
      mockInvoke.mockRejectedValueOnce(new Error("boom"));
      // The finally refresh reads the true mode (Rust never left local).
      mockInvoke.mockResolvedValueOnce({ type: "local" });

      await expect(playbackMode.transferToRemote("session-456")).rejects.toThrow("boom");

      // The reconciling read must have happened despite the throw.
      expect(mockInvoke).toHaveBeenCalledWith("playback_mode_get_current");
      const state = get(playbackMode);
      expect(state.mode).toBe("local");
    });
  });

  describe("clearError", () => {
    it("should clear transfer error", async () => {
      const { playbackMode } = await import("./playbackMode");

      // Set up remote mode and simulate a failed transfer to set an error
      playbackMode.setMode("remote", "session-123");
      mockInvoke.mockRejectedValueOnce(new Error("Test error"));

      try {
        await playbackMode.disconnect();
      } catch {
        // Expected to throw
      }

      let state = get(playbackMode);
      expect(state.transferError).toBe("Test error");

      // Clear the error
      playbackMode.clearError();

      state = get(playbackMode);
      expect(state.transferError).toBeNull();
    });
  });

  describe("derived stores", () => {
    it("isRemoteMode should be true when mode is remote", async () => {
      const { playbackMode, isRemoteMode } = await import("./playbackMode");

      playbackMode.setMode("remote", "session-123");

      const isRemote = get(isRemoteMode);
      expect(isRemote).toBe(true);
    });

    it("isLocalMode should be true when mode is local", async () => {
      const { playbackMode, isLocalMode } = await import("./playbackMode");

      playbackMode.setMode("local");

      const isLocal = get(isLocalMode);
      expect(isLocal).toBe(true);
    });

    it("isIdleMode should be true when mode is idle", async () => {
      const { playbackMode, isIdleMode } = await import("./playbackMode");

      playbackMode.setMode("idle");

      const isIdle = get(isIdleMode);
      expect(isIdle).toBe(true);
    });
  });
});
