import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { invoke } from "@tauri-apps/api/core";
import { RepositoryClient } from "./repository-client";

vi.mock("@tauri-apps/api/core");

const mockInvoke = vi.mocked(invoke);

describe("RepositoryClient", () => {
  let client: RepositoryClient;

  beforeEach(() => {
    client = new RepositoryClient();
    vi.clearAllMocks();
  });

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

  describe("Initialization", () => {
    it("should initialize with no handle", () => {
      expect(() => client.getHandle()).toThrow("Repository not initialized");
    });

    it("should create repository with invoke command", async () => {
      const mockHandle = "test-handle-123";
      (invoke as any).mockResolvedValueOnce(mockHandle);

      const handle = await client.create("https://server.com", "user1", "token123", "server1");

      expect(handle).toBe(mockHandle);
      expect(invoke).toHaveBeenCalledWith("repository_create", {
        serverUrl: "https://server.com",
        userId: "user1",
        accessToken: "token123",
        serverId: "server1",
      });
    });

    it("should store handle after creation", async () => {
      const mockHandle = "test-handle-456";
      (invoke as any).mockResolvedValueOnce(mockHandle);

      await client.create("https://server.com", "user1", "token123", "server1");

      expect(client.getHandle()).toBe(mockHandle);
    });

    it("should destroy repository and clear handle", async () => {
      const mockHandle = "test-handle-789";
      (invoke as any).mockResolvedValueOnce(mockHandle);
      await client.create("https://server.com", "user1", "token123", "server1");

      (invoke as any).mockResolvedValueOnce(undefined);
      await client.destroy();

      expect(() => client.getHandle()).toThrow("Repository not initialized");
      expect(invoke).toHaveBeenCalledWith("repository_destroy", { handle: mockHandle });
    });
  });

  describe("Image URL Methods", () => {
    beforeEach(async () => {
      (invoke as any).mockResolvedValueOnce("test-handle-123");
      await client.create("https://server.com", "user1", "token123", "server1");
    });

    it("should get image URL from backend", async () => {
      const mockUrl = "https://server.com/Items/item123/Images/Primary?maxWidth=300&api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const imageUrl = await client.getImageUrl("item123", "Primary", { maxWidth: 300 });

      expect(imageUrl).toBe(mockUrl);
      expect(invoke).toHaveBeenCalledWith("repository_get_image_url", {
        handle: "test-handle-123",
        itemId: "item123",
        imageType: "Primary",
        options: { maxWidth: 300 },
      });
    });

    it("should use default image type if not provided", async () => {
      const mockUrl = "https://server.com/Items/item123/Images/Primary?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      await client.getImageUrl("item123");

      expect(invoke).toHaveBeenCalledWith("repository_get_image_url", {
        handle: "test-handle-123",
        itemId: "item123",
        imageType: "Primary",
        options: null,
      });
    });

    it("should pass multiple image options to backend", async () => {
      const mockUrl = "https://server.com/Items/item123/Images/Backdrop?maxWidth=1920&maxHeight=1080&quality=90&api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const options = {
        maxWidth: 1920,
        maxHeight: 1080,
        quality: 90,
        tag: "abc123",
      };

      await client.getImageUrl("item123", "Backdrop", options);

      expect(invoke).toHaveBeenCalledWith("repository_get_image_url", {
        handle: "test-handle-123",
        itemId: "item123",
        imageType: "Backdrop",
        options,
      });
    });

    it("should handle different image types", async () => {
      const mockUrl = "https://server.com/Items/item123/Images/Logo?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      await client.getImageUrl("item123", "Logo");

      expect(invoke).toHaveBeenCalledWith("repository_get_image_url", {
        handle: expect.any(String),
        itemId: "item123",
        imageType: "Logo",
        options: null,
      });
    });

    it("should throw error if not initialized before getImageUrl", async () => {
      const newClient = new RepositoryClient();

      await expect(newClient.getImageUrl("item123")).rejects.toThrow(
        "Repository not initialized"
      );
    });
  });

  describe("Subtitle URL Methods", () => {
    beforeEach(async () => {
      (invoke as any).mockResolvedValueOnce("test-handle-123");
      await client.create("https://server.com", "user1", "token123", "server1");
    });

    it("should get subtitle URL from backend", async () => {
      const mockUrl = "https://server.com/Videos/item123/Subtitles/1/subtitles.vtt?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const subtitleUrl = await client.getSubtitleUrl("item123", "source456", 0);

      expect(subtitleUrl).toBe(mockUrl);
      expect(invoke).toHaveBeenCalledWith("repository_get_subtitle_url", {
        handle: "test-handle-123",
        itemId: "item123",
        mediaSourceId: "source456",
        streamIndex: 0,
        format: "vtt",
      });
    });

    it("should use default format if not provided", async () => {
      const mockUrl = "https://server.com/Videos/item123/Subtitles/0/subtitles.vtt?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      await client.getSubtitleUrl("item123", "source456", 0);

      expect(invoke).toHaveBeenCalledWith("repository_get_subtitle_url", {
        handle: expect.any(String),
        itemId: "item123",
        mediaSourceId: "source456",
        streamIndex: 0,
        format: "vtt",
      });
    });

    it("should support custom subtitle formats", async () => {
      const mockUrl = "https://server.com/Videos/item123/Subtitles/0/subtitles.srt?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      await client.getSubtitleUrl("item123", "source456", 1, "srt");

      expect(invoke).toHaveBeenCalledWith("repository_get_subtitle_url", {
        handle: expect.any(String),
        itemId: "item123",
        mediaSourceId: "source456",
        streamIndex: 1,
        format: "srt",
      });
    });
  });

  describe("Video Download URL Methods", () => {
    beforeEach(async () => {
      (invoke as any).mockResolvedValueOnce("test-handle-123");
      await client.create("https://server.com", "user1", "token123", "server1");
    });

    it("should get video download URL from backend", async () => {
      const mockUrl = "https://server.com/Videos/item123/stream.mp4?maxWidth=1920&api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const downloadUrl = await client.getVideoDownloadUrl("item123", "high");

      expect(downloadUrl).toBe(mockUrl);
      expect(invoke).toHaveBeenCalledWith("repository_get_video_download_url", {
        handle: "test-handle-123",
        itemId: "item123",
        quality: "high",
        mediaSourceId: null,
      });
    });

    it("should use original quality by default", async () => {
      const mockUrl = "https://server.com/Videos/item123/stream.mp4?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      await client.getVideoDownloadUrl("item123");

      expect(invoke).toHaveBeenCalledWith("repository_get_video_download_url", {
        handle: expect.any(String),
        itemId: "item123",
        quality: "original",
        mediaSourceId: null,
      });
    });

    it("should support quality presets", async () => {
      const qualities = ["original", "high", "medium", "low"];

      for (const quality of qualities) {
        vi.clearAllMocks();
        (invoke as any).mockResolvedValueOnce("test-handle-123");
        await client.create("https://server.com", "user1", "token123", "server1");

        (invoke as any).mockResolvedValueOnce(`https://server.com/stream.mp4?quality=${quality}`);

        await client.getVideoDownloadUrl("item123", quality as any);

        expect(invoke).toHaveBeenCalledWith(
          "repository_get_video_download_url",
          expect.objectContaining({
            quality,
          })
        );
      }
    });

    it("should support optional media source ID", async () => {
      const mockUrl = "https://server.com/Videos/item123/stream.mp4?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      await client.getVideoDownloadUrl("item123", "medium", "source789");

      expect(invoke).toHaveBeenCalledWith("repository_get_video_download_url", {
        handle: expect.any(String),
        itemId: "item123",
        quality: "medium",
        mediaSourceId: "source789",
      });
    });
  });

  describe("Library Methods", () => {
    beforeEach(async () => {
      (invoke as any).mockResolvedValueOnce("test-handle-123");
      await client.create("https://server.com", "user1", "token123", "server1");
    });

    it("should get libraries from backend", async () => {
      const mockLibraries = [
        { id: "lib1", name: "Music", collectionType: "music" },
        { id: "lib2", name: "Movies", collectionType: "movies" },
      ];
      (invoke as any).mockResolvedValueOnce(mockLibraries);

      const libraries = await client.getLibraries();

      expect(libraries).toEqual(mockLibraries);
      expect(invoke).toHaveBeenCalledWith("repository_get_libraries", {
        handle: "test-handle-123",
      });
    });

    it("should get items with sorting parameters", async () => {
      const mockResult = {
        items: [
          { id: "item1", name: "Track 1", type: "Audio" },
          { id: "item2", name: "Track 2", type: "Audio" },
        ],
        totalRecordCount: 2,
      };
      (invoke as any).mockResolvedValueOnce(mockResult);

      const result = await client.getItems("library123", {
        sortBy: "SortName",
        sortOrder: "Ascending",
        limit: 50,
      });

      expect(result).toEqual(mockResult);
      expect(invoke).toHaveBeenCalledWith("repository_get_items", {
        handle: "test-handle-123",
        parentId: "library123",
        options: {
          sortBy: "SortName",
          sortOrder: "Ascending",
          limit: 50,
        },
      });
    });

    it("should search with backend search command", async () => {
      const mockResult = {
        items: [
          { id: "item1", name: "Search Result 1", type: "Audio" },
        ],
        totalRecordCount: 1,
      };
      (invoke as any).mockResolvedValueOnce(mockResult);

      const result = await client.search("query", {
        includeItemTypes: ["Audio"],
        limit: 100,
      });

      expect(result).toEqual(mockResult);
      expect(invoke).toHaveBeenCalledWith("repository_search", {
        handle: "test-handle-123",
        query: "query",
        options: {
          includeItemTypes: ["Audio"],
          limit: 100,
        },
        requestId: 0,
      });
    });

    // Downloaded-only browse path (UR-055 | DR-082) — verifies command names and
    // camelCase params per the Tauri v2 rule (CLAUDE.md).
    it("should get downloaded libraries from backend", async () => {
      const mockLibraries = [{ id: "lib1", name: "Music", collectionType: "music" }];
      (invoke as any).mockResolvedValueOnce(mockLibraries);

      const libraries = await client.getDownloadedLibraries();

      expect(libraries).toEqual(mockLibraries);
      expect(invoke).toHaveBeenCalledWith("repository_get_downloaded_libraries", {
        handle: "test-handle-123",
      });
    });

    it("should get downloaded items with camelCase params", async () => {
      const mockResult = { items: [{ id: "t1", name: "Track", type: "Audio" }], totalRecordCount: 1 };
      (invoke as any).mockResolvedValueOnce(mockResult);

      const result = await client.getDownloadedItems("album1", { limit: 50 });

      expect(result).toEqual(mockResult);
      expect(invoke).toHaveBeenCalledWith("repository_get_downloaded_items", {
        handle: "test-handle-123",
        parentId: "album1",
        options: { limit: 50 },
      });
    });

    it("should get download disk usage from backend", async () => {
      const mockUsage = { sizes: { t1: 1000 }, partialContainers: {}, deviceTotalBytes: 1000, itemCount: 1 };
      (invoke as any).mockResolvedValueOnce(mockUsage);

      const usage = await client.getDownloadDiskUsage();

      expect(usage).toEqual(mockUsage);
      expect(invoke).toHaveBeenCalledWith("repository_get_download_disk_usage", {
        handle: "test-handle-123",
      });
    });
  });

  describe("Playback Methods", () => {
    beforeEach(async () => {
      (invoke as any).mockResolvedValueOnce("test-handle-123");
      await client.create("https://server.com", "user1", "token123", "server1");
    });

    it("should get audio stream URL", async () => {
      const mockUrl = "https://server.com/Audio/item123/stream.mp3?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const url = await client.getAudioStreamUrl("item123");

      expect(url).toBe(mockUrl);
      expect(invoke).toHaveBeenCalledWith("repository_get_audio_stream_url", {
        handle: "test-handle-123",
        itemId: "item123",
      });
    });

    it("should get video stream URL", async () => {
      const mockUrl = "https://server.com/Videos/item123/stream.mp4?api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const url = await client.getVideoStreamUrl("item123");

      expect(url).toBe(mockUrl);
      expect(invoke).toHaveBeenCalledWith("repository_get_video_stream_url", {
        handle: "test-handle-123",
        itemId: "item123",
        mediaSourceId: null,
        startTimeSeconds: null,
        audioStreamIndex: null,
      });
    });

    it("should get video stream URL with options", async () => {
      const mockUrl = "https://server.com/Videos/item123/stream.mp4?start=300&api_key=token";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const url = await client.getVideoStreamUrl("item123", "source456", 300, 0);

      expect(url).toBe(mockUrl);
      expect(invoke).toHaveBeenCalledWith("repository_get_video_stream_url", {
        handle: "test-handle-123",
        itemId: "item123",
        mediaSourceId: "source456",
        startTimeSeconds: 300,
        audioStreamIndex: 0,
      });
    });

    it("should get audio-only stream URL for a video item (camelCase params)", async () => {
      // TRACES: UR-040 | JA-032 | UT-061
      const mockUrl = "https://server.com/Audio/item123/universal?AudioStreamIndex=2";
      (invoke as any).mockResolvedValueOnce(mockUrl);

      const url = await client.getAudioOnlyStreamUrlForVideo("item123", "source456", 193, 2);

      expect(url).toBe(mockUrl);
      expect(invoke).toHaveBeenCalledWith("repository_get_audio_only_stream_url_for_video", {
        handle: "test-handle-123",
        itemId: "item123",
        mediaSourceId: "source456",
        startTimeSeconds: 193,
        audioStreamIndex: 2,
      });
    });

    it("should default optional params to null for audio-only stream URL", async () => {
      // TRACES: UR-040 | JA-032 | UT-061
      (invoke as any).mockResolvedValueOnce("https://server.com/Audio/item123/universal");

      await client.getAudioOnlyStreamUrlForVideo("item123");

      expect(invoke).toHaveBeenCalledWith("repository_get_audio_only_stream_url_for_video", {
        handle: "test-handle-123",
        itemId: "item123",
        mediaSourceId: null,
        startTimeSeconds: null,
        audioStreamIndex: null,
      });
    });

    it("should report playback progress", async () => {
      (invoke as any).mockResolvedValueOnce(undefined);

      await client.reportPlaybackProgress("item123", 5000000);

      expect(invoke).toHaveBeenCalledWith("repository_report_playback_progress", {
        handle: "test-handle-123",
        itemId: "item123",
        positionTicks: 5000000,
      });
    });
  });

  describe("Playlist Methods", () => {
    beforeEach(async () => {
      (invoke as any).mockResolvedValueOnce("test-handle-123");
      await client.create("https://server.com", "user1", "token123", "server1");
    });

    it("should create a playlist", async () => {
      const mockResult = { id: "playlist-001" };
      (invoke as any).mockResolvedValueOnce(mockResult);

      const result = await client.createPlaylist("My Playlist", ["track1", "track2"]);

      expect(result).toEqual(mockResult);
      expect(invoke).toHaveBeenCalledWith("playlist_create", {
        handle: "test-handle-123",
        name: "My Playlist",
        itemIds: ["track1", "track2"],
      });
    });

    it("should create a playlist without initial items", async () => {
      const mockResult = { id: "playlist-002" };
      (invoke as any).mockResolvedValueOnce(mockResult);

      await client.createPlaylist("Empty Playlist");

      expect(invoke).toHaveBeenCalledWith("playlist_create", {
        handle: "test-handle-123",
        name: "Empty Playlist",
        itemIds: null,
      });
    });

    it("should delete a playlist", async () => {
      (invoke as any).mockResolvedValueOnce(undefined);

      await client.deletePlaylist("playlist-001");

      expect(invoke).toHaveBeenCalledWith("playlist_delete", {
        handle: "test-handle-123",
        playlistId: "playlist-001",
      });
    });

    it("should rename a playlist", async () => {
      (invoke as any).mockResolvedValueOnce(undefined);

      await client.renamePlaylist("playlist-001", "New Name");

      expect(invoke).toHaveBeenCalledWith("playlist_rename", {
        handle: "test-handle-123",
        playlistId: "playlist-001",
        name: "New Name",
      });
    });

    it("should get playlist items", async () => {
      const mockItems = [
        { playlistItemId: "entry1", id: "track1", name: "Track 1", type: "Audio" },
        { playlistItemId: "entry2", id: "track2", name: "Track 2", type: "Audio" },
      ];
      (invoke as any).mockResolvedValueOnce(mockItems);

      const items = await client.getPlaylistItems("playlist-001");

      expect(items).toEqual(mockItems);
      expect(invoke).toHaveBeenCalledWith("playlist_get_items", {
        handle: "test-handle-123",
        playlistId: "playlist-001",
      });
    });

    it("should add items to a playlist", async () => {
      (invoke as any).mockResolvedValueOnce(undefined);

      await client.addToPlaylist("playlist-001", ["track3", "track4"]);

      expect(invoke).toHaveBeenCalledWith("playlist_add_items", {
        handle: "test-handle-123",
        playlistId: "playlist-001",
        itemIds: ["track3", "track4"],
      });
    });

    it("should remove items from a playlist using entry IDs", async () => {
      (invoke as any).mockResolvedValueOnce(undefined);

      await client.removeFromPlaylist("playlist-001", ["entry1", "entry2"]);

      expect(invoke).toHaveBeenCalledWith("playlist_remove_items", {
        handle: "test-handle-123",
        playlistId: "playlist-001",
        entryIds: ["entry1", "entry2"],
      });
    });

    it("should move a playlist item", async () => {
      (invoke as any).mockResolvedValueOnce(undefined);

      await client.movePlaylistItem("playlist-001", "track1", 3);

      expect(invoke).toHaveBeenCalledWith("playlist_move_item", {
        handle: "test-handle-123",
        playlistId: "playlist-001",
        itemId: "track1",
        newIndex: 3,
      });
    });

    it("should throw error if not initialized before playlist operations", async () => {
      const newClient = new RepositoryClient();
      await expect(newClient.getPlaylistItems("pl-1")).rejects.toThrow("Repository not initialized");
      await expect(newClient.createPlaylist("test")).rejects.toThrow("Repository not initialized");
      await expect(newClient.deletePlaylist("pl-1")).rejects.toThrow("Repository not initialized");
    });
  });

  describe("Error Handling", () => {
    it("should throw error if invoke fails", async () => {
      (invoke as any).mockRejectedValueOnce(new Error("Network error"));

      await expect(client.create("https://server.com", "user1", "token", "server1")).rejects.toThrow(
        "Network error"
      );
    });

    it("should handle missing optional parameters", async () => {
      (invoke as any).mockResolvedValueOnce("test-handle-123");
      await client.create("https://server.com", "user1", "token123", "server1");

      (invoke as any).mockResolvedValueOnce("");

      await client.getImageUrl("item123");

      expect(invoke).toHaveBeenCalledWith(
        "repository_get_image_url",
        expect.objectContaining({
          options: null,
        })
      );
    });
  });
});
