/**
 * Tests for PlayerPrivacyManager.
 * @module tests/unit/core/PlayerPrivacyManager.test
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { PlayerPrivacyManager } from "../../../src/core/PlayerPrivacyManager.js";
import { createFoundryAdapterMock } from "../../helpers/foundryAdapterMock.js";
import { PRIVACY_SETTINGS_DEFAULT } from "../../../src/contracts/privacy-settings.js";

describe("PlayerPrivacyManager", () => {
  /** @type {import('../../../src/foundry/FoundryAdapter.js').FoundryAdapter} */
  let adapter;
  /** @type {PlayerPrivacyManager} */
  let manager;

  beforeEach(() => {
    adapter = createFoundryAdapterMock({
      users: {
        get: vi.fn(),
        all: vi.fn(),
      },
    });
  });

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

  describe("constructor", () => {
    it("should construct with valid adapter", () => {
      expect(() => new PlayerPrivacyManager(adapter)).not.toThrow();
    });

    it("should throw TypeError for null adapter", () => {
      expect(() => new PlayerPrivacyManager(null)).toThrow(TypeError);
      expect(() => new PlayerPrivacyManager(null)).toThrow(
        "PlayerPrivacyManager: adapter argument is required and must be an object"
      );
    });

    it("should throw TypeError for undefined adapter", () => {
      expect(() => new PlayerPrivacyManager(undefined)).toThrow(TypeError);
      expect(() => new PlayerPrivacyManager(undefined)).toThrow(
        "PlayerPrivacyManager: adapter argument is required and must be an object"
      );
    });

    it("should throw TypeError for non-object adapter", () => {
      expect(() => new PlayerPrivacyManager("not an object")).toThrow(TypeError);
      expect(() => new PlayerPrivacyManager("not an object")).toThrow(
        "PlayerPrivacyManager: adapter argument is required and must be an object"
      );
    });

    it("should throw TypeError when adapter.users is not an object", () => {
      const badAdapter = { users: "not an object" };
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(TypeError);
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(
        "PlayerPrivacyManager: adapter.users must be an object"
      );
    });

    it("should throw TypeError when adapter.users is null", () => {
      const badAdapter = { users: null };
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(TypeError);
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(
        "PlayerPrivacyManager: adapter.users must be an object"
      );
    });

    it("should throw TypeError when adapter.users.get is not a function", () => {
      const badAdapter = { users: { get: "not a function", all: vi.fn() } };
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(TypeError);
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(
        "PlayerPrivacyManager: adapter.users.get must be a function"
      );
    });

    it("should throw TypeError when adapter.users.all is not a function", () => {
      const badAdapter = { users: { get: vi.fn(), all: "not a function" } };
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(TypeError);
      expect(() => new PlayerPrivacyManager(badAdapter)).toThrow(
        "PlayerPrivacyManager: adapter.users.all must be a function"
      );
    });
  });

  describe("getSettings", () => {
    beforeEach(() => {
      manager = new PlayerPrivacyManager(adapter);
    });

    it("should return default settings when no flag exists", () => {
      adapter.users.get.mockReturnValue(null);
      const result = manager.getSettings("user1");
      expect(result).toEqual(PRIVACY_SETTINGS_DEFAULT);
    });

    it("should return saved settings when flag exists", () => {
      const savedSettings = { reactionCamEnabled: true, customPortraitFallback: null };
      adapter.users.get.mockReturnValue({
        getFlag: vi.fn((scope, key) => {
          if (scope === "scrying-pool") {
            return savedSettings[key];
          }
          return undefined;
        }),
      });
      const result = manager.getSettings("user1");
      expect(result).toEqual(savedSettings);
    });

    it("should return partial settings merged with defaults", () => {
      adapter.users.get.mockReturnValue({
        getFlag: vi.fn((scope, key) => {
          if (scope === "scrying-pool" && key === "reactionCamEnabled") {
            return true;
          }
          return undefined;
        }),
      });
      const result = manager.getSettings("user1");
      expect(result).toEqual({
        reactionCamEnabled: true,
        customPortraitFallback: null,
      });
    });

    it("should handle null user gracefully", () => {
      adapter.users.get.mockReturnValue(null);
      const result = manager.getSettings("nonexistent");
      expect(result).toEqual(PRIVACY_SETTINGS_DEFAULT);
    });

    it("should handle user without getFlag method", () => {
      adapter.users.get.mockReturnValue({});
      const result = manager.getSettings("user1");
      expect(result).toEqual(PRIVACY_SETTINGS_DEFAULT);
    });
  });

  describe("setSetting", () => {
    beforeEach(() => {
      manager = new PlayerPrivacyManager(adapter);
    });

    it("should validate key is known", async () => {
      adapter.users.get.mockReturnValue({
        setFlag: vi.fn().mockResolvedValue(undefined),
      });
      await expect(
        manager.setSetting("user1", "invalidKey", true)
      ).rejects.toThrow(TypeError);
      await expect(
        manager.setSetting("user1", "invalidKey", true)
      ).rejects.toThrow("unknown key");
    });

    it("should validate value is boolean", async () => {
      adapter.users.get.mockReturnValue({
        setFlag: vi.fn().mockResolvedValue(undefined),
      });
      await expect(
        manager.setSetting("user1", "reactionCamEnabled", "not boolean")
      ).rejects.toThrow(TypeError);
      await expect(
        manager.setSetting("user1", "reactionCamEnabled", "not boolean")
      ).rejects.toThrow("value must be a boolean");
    });

    it("should call adapter.users.get with userId", async () => {
      const mockUser = { setFlag: vi.fn().mockResolvedValue(undefined) };
      adapter.users.get.mockReturnValue(mockUser);
      await manager.setSetting("user1", "reactionCamEnabled", true);
      expect(adapter.users.get).toHaveBeenCalledWith("user1");
    });

    it("should call user.setFlag with correct parameters", async () => {
      const mockUser = { setFlag: vi.fn().mockResolvedValue(undefined) };
      adapter.users.get.mockReturnValue(mockUser);
      await manager.setSetting("user1", "reactionCamEnabled", true);
      expect(mockUser.setFlag).toHaveBeenCalledWith(
        "scrying-pool",
        "reactionCamEnabled",
        true
      );
    });

    it("should throw when user does not exist", async () => {
      adapter.users.get.mockReturnValue(null);
      await expect(
        manager.setSetting("nonexistent", "reactionCamEnabled", true)
      ).rejects.toThrow(TypeError);
      await expect(
        manager.setSetting("nonexistent", "reactionCamEnabled", true)
      ).rejects.toThrow("User 'nonexistent' not found");
    });

    it("should throw when user does not have setFlag method", async () => {
      adapter.users.get.mockReturnValue({});
      await expect(
        manager.setSetting("user1", "reactionCamEnabled", true)
      ).rejects.toThrow(TypeError);
      await expect(
        manager.setSetting("user1", "reactionCamEnabled", true)
      ).rejects.toThrow("User 'user1' does not support setFlag");
    });

    it("should emit change event after successful update", async () => {
      const mockUser = { setFlag: vi.fn().mockResolvedValue(undefined) };
      adapter.users.get.mockReturnValue(mockUser);
      // First call to getSettings returns false (default), then we set to true
      adapter.users.get.mockReturnValue({
        getFlag: vi.fn(() => false),
        setFlag: vi.fn().mockResolvedValue(undefined),
      });
      const callback = vi.fn();
      manager.onChange(callback);
      await manager.setSetting("user1", "reactionCamEnabled", true);
      expect(callback).toHaveBeenCalledWith("user1", "reactionCamEnabled", true, false);
    });
  });

  describe("isOptedIn", () => {
    beforeEach(() => {
      manager = new PlayerPrivacyManager(adapter);
    });

    it("should throw for unknown feature", () => {
      adapter.users.get.mockReturnValue({
        getFlag: vi.fn(() => false),
      });
      expect(() => manager.isOptedIn("user1", "invalidFeature")).toThrow(TypeError);
      expect(() => manager.isOptedIn("user1", "invalidFeature")).toThrow("unknown feature");
    });

    it("should return true when setting is enabled", () => {
      adapter.users.get.mockReturnValue({
        getFlag: vi.fn((scope, key) => {
          if (key === "reactionCamEnabled") return true;
          return false;
        }),
      });
      expect(manager.isOptedIn("user1", "reactionCam")).toBe(true);
    });

    it("should return false when setting is disabled", () => {
      adapter.users.get.mockReturnValue({
        getFlag: vi.fn((scope, key) => {
          if (key === "reactionCamEnabled") return false;
          return false;
        }),
      });
      expect(manager.isOptedIn("user1", "reactionCam")).toBe(false);
    });

    it("should return false when setting is not found (defaults to false)", () => {
      adapter.users.get.mockReturnValue({
        getFlag: vi.fn(() => undefined),
      });
      expect(manager.isOptedIn("user1", "reactionCam")).toBe(false);
    });

    it("should return false for non-existent user", () => {
      adapter.users.get.mockReturnValue(null);
      expect(manager.isOptedIn("nonexistent", "reactionCam")).toBe(false);
    });

  });

  describe("getAllSettings", () => {
    beforeEach(() => {
      manager = new PlayerPrivacyManager(adapter);
    });

    it("should return empty Map when no users", () => {
      adapter.users.all.mockReturnValue([]);
      const result = manager.getAllSettings();
      expect(result).toEqual(new Map());
    });

    it("should aggregate settings from all users", () => {
      const user1 = {
        id: "user1",
        getFlag: vi.fn((scope, key) => {
          if (key === "reactionCamEnabled") return true;
          return undefined; // customPortraitFallback and other keys
        }),
      };
      const user2 = {
        id: "user2",
        getFlag: vi.fn((scope, key) => {
          if (key === "reactionCamEnabled") return false;
          return undefined; // customPortraitFallback and other keys
        }),
      };
      adapter.users.all.mockReturnValue([user1, user2]);
      adapter.users.get.mockImplementation((id) => {
        if (id === "user1") return user1;
        if (id === "user2") return user2;
        return null;
      });

      const result = manager.getAllSettings();
      expect(result.size).toBe(2);
      expect(result.get("user1")).toEqual({
        reactionCamEnabled: true,
        customPortraitFallback: null,
      });
      expect(result.get("user2")).toEqual({
        reactionCamEnabled: false,
        customPortraitFallback: null,
      });
    });

    it("should skip users without getFlag method", () => {
      const user1 = {
        id: "user1",
        getFlag: vi.fn(() => true),
      };
      const user2 = { id: "user2" };
      adapter.users.all.mockReturnValue([user1, user2]);
      adapter.users.get.mockImplementation((id) => {
        if (id === "user1") return user1;
        if (id === "user2") return user2;
        return null;
      });

      const result = manager.getAllSettings();
      expect(result.size).toBe(1);
      expect(result.has("user1")).toBe(true);
      expect(result.has("user2")).toBe(false);
    });
  });

  describe("onChange", () => {
    beforeEach(() => {
      manager = new PlayerPrivacyManager(adapter);
    });

    it("should allow multiple subscribers", () => {
      const callback1 = vi.fn();
      const callback2 = vi.fn();
      manager.onChange(callback1);
      manager.onChange(callback2);

      const mockUser = { setFlag: vi.fn().mockResolvedValue(undefined) };
      adapter.users.get.mockReturnValue(mockUser);

      return manager.setSetting("user1", "reactionCamEnabled", true).then(() => {
        expect(callback1).toHaveBeenCalled();
        expect(callback2).toHaveBeenCalled();
      });
    });

    it("should pass correct parameters to subscribers", async () => {
      const callback = vi.fn();
      manager.onChange(callback);

      const mockUser = {
        getFlag: vi.fn(() => false),
        setFlag: vi.fn().mockResolvedValue(undefined),
      };
      adapter.users.get.mockReturnValue(mockUser);

      await manager.setSetting("user1", "reactionCamEnabled", true);
      expect(callback).toHaveBeenCalledWith("user1", "reactionCamEnabled", true, false);
    });

    it("should allow unsubscribing", async () => {
      const callback = vi.fn();
      const unsubscribe = manager.onChange(callback);

      const mockUser = {
        getFlag: vi.fn(() => false),
        setFlag: vi.fn().mockResolvedValue(undefined),
      };
      adapter.users.get.mockReturnValue(mockUser);

      // Subscribe, unsubscribe, then set setting
      unsubscribe();
      await manager.setSetting("user1", "reactionCamEnabled", true);
      expect(callback).not.toHaveBeenCalled();
    });
  });

  describe("Integration scenarios", () => {
    beforeEach(() => {
      manager = new PlayerPrivacyManager(adapter);
    });

    it("should handle player enabling Reaction Cam", async () => {
      const mockUser = {
        id: "player1",
        getFlag: vi.fn(() => false),
        setFlag: vi.fn().mockResolvedValue(undefined),
      };
      adapter.users.get.mockReturnValue(mockUser);

      // Initially opted out
      expect(manager.isOptedIn("player1", "reactionCam")).toBe(false);

      // Enable Reaction Cam
      await manager.setSetting("player1", "reactionCamEnabled", true);
      // After setting, the mock should return true for reactionCamEnabled
      adapter.users.get.mockReturnValue({
        id: "player1",
        getFlag: vi.fn((scope, key) => {
          if (key === "reactionCamEnabled") return true;
          return false;
        }),
        setFlag: vi.fn().mockResolvedValue(undefined),
      });
      expect(manager.isOptedIn("player1", "reactionCam")).toBe(true);
    });

    it("should allow GM to view all players' settings", () => {
      const gm = { id: "gm1", isGM: true, getFlag: vi.fn(() => false) };
      const player1 = {
        id: "player1",
        getFlag: vi.fn((scope, key) => (key === "reactionCamEnabled" ? true : false)),
      };
      const player2 = {
        id: "player2",
        getFlag: vi.fn((scope, key) => (key === "reactionCamEnabled" ? false : false)),
      };

      adapter.users.all.mockReturnValue([gm, player1, player2]);
      adapter.users.get.mockImplementation((id) => {
        if (id === "gm1") return gm;
        if (id === "player1") return player1;
        if (id === "player2") return player2;
        return null;
      });

      const allSettings = manager.getAllSettings();
      expect(allSettings.size).toBe(3);
    });
  });

  // ==================== PORTRAIT FALLBACK TESTS ====================

  describe("portrait fallback methods", () => {
    beforeEach(() => {
      manager = new PlayerPrivacyManager(adapter);
    });

    describe("setPortraitFallback", () => {
      it("should validate DataURL format", async () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => null),
          setFlag: vi.fn().mockResolvedValue(undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const invalidDataURL = "https://example.com/image.png";
        await expect(
          manager.setPortraitFallback("player1", invalidDataURL)
        ).rejects.toThrow(TypeError);
      });

      it("should accept valid PNG DataURL", async () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => null),
          setFlag: vi.fn().mockResolvedValue(undefined),
          unsetFlag: vi.fn().mockResolvedValue(undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const dataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==";
        await expect(
          manager.setPortraitFallback("player1", dataURL)
        ).resolves.not.toThrow();

        expect(mockUser.setFlag).toHaveBeenCalledWith(
          "scrying-pool",
          "customPortraitFallback",
          dataURL
        );
      });

      it("should emit change event with type 'portrait'", async () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => null),
          setFlag: vi.fn().mockResolvedValue(undefined),
          unsetFlag: vi.fn().mockResolvedValue(undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const changeCallback = vi.fn();
        manager.onChange(changeCallback);

        const dataURL = "data:image/png;base64,test";
        await manager.setPortraitFallback("player1", dataURL);

        expect(changeCallback).toHaveBeenCalled();
        // Check that the callback was called with portrait-related parameters
        const calls = changeCallback.mock.calls;
        expect(calls.length).toBeGreaterThan(0);
      });

      it("should reject invalid MIME type", async () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => null),
          setFlag: vi.fn().mockResolvedValue(undefined),
          unsetFlag: vi.fn().mockResolvedValue(undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const svgDataURL = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==";
        await expect(
          manager.setPortraitFallback("player1", svgDataURL)
        ).rejects.toThrow(TypeError);
      });

      it("should throw TypeError for non-existent user", async () => {
        adapter.users.get.mockReturnValue(null);

        const dataURL = "data:image/png;base64,test";
        await expect(
          manager.setPortraitFallback("nonexistent", dataURL)
        ).rejects.toThrow(TypeError);
      });
    });

    describe("getPortraitFallback", () => {
      it("should return null when no custom portrait is set", () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn((scope, key) => {
            if (key === "customPortraitFallback") return undefined;
            return undefined;
          }),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const result = manager.getPortraitFallback("player1");
        expect(result).toBeNull();
      });

      it("should return DataURL when custom portrait is set", () => {
        const dataURL = "data:image/png;base64,test123";
        const mockUser = {
          id: "player1",
          getFlag: vi.fn((scope, key) => {
            if (key === "customPortraitFallback") return dataURL;
            return undefined;
          }),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const result = manager.getPortraitFallback("player1");
        expect(result).toBe(dataURL);
      });

      it("should return null for non-existent user", () => {
        adapter.users.get.mockReturnValue(null);

        const result = manager.getPortraitFallback("nonexistent");
        expect(result).toBeNull();
      });
    });

    describe("getPortraitFallbackDataURL", () => {
      it("should return DataURL directly", () => {
        const dataURL = "data:image/png;base64,test123";
        const mockUser = {
          id: "player1",
          getFlag: vi.fn((scope, key) => {
            if (key === "customPortraitFallback") return dataURL;
            return undefined;
          }),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const result = manager.getPortraitFallbackDataURL("player1");
        expect(result).toBe(dataURL);
      });

      it("should return null when not set", () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const result = manager.getPortraitFallbackDataURL("player1");
        expect(result).toBeNull();
      });
    });

    describe("removePortraitFallback", () => {
      it("should call unsetFlag for customPortraitFallback", async () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => "data:image/png;base64,old"),
          unsetFlag: vi.fn().mockResolvedValue(undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        await manager.removePortraitFallback("player1");

        expect(mockUser.unsetFlag).toHaveBeenCalledWith(
          "scrying-pool",
          "customPortraitFallback"
        );
      });

      it("should emit change event with type 'portrait'", async () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => "data:image/png;base64,old"),
          unsetFlag: vi.fn().mockResolvedValue(undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const changeCallback = vi.fn();
        manager.onChange(changeCallback);

        await manager.removePortraitFallback("player1");

        expect(changeCallback).toHaveBeenCalled();
      });

      it("should throw TypeError for non-existent user", async () => {
        adapter.users.get.mockReturnValue(null);

        await expect(
          manager.removePortraitFallback("nonexistent")
        ).rejects.toThrow(TypeError);
      });
    });

    describe("getSettings with portrait fallback", () => {
      it("should include customPortraitFallback in returned settings", () => {
        const dataURL = "data:image/png;base64,test";
        const mockUser = {
          id: "player1",
          getFlag: vi.fn((scope, key) => {
            if (key === "customPortraitFallback") return dataURL;
            if (key === "reactionCamEnabled") return true;
            return undefined;
          }),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const settings = manager.getSettings("player1");
        expect(settings).toHaveProperty("customPortraitFallback");
        expect(settings.customPortraitFallback).toBe(dataURL);
      });
    });

    describe("setSetting rejection of customPortraitFallback", () => {
      it("should reject customPortraitFallback key in setSetting", async () => {
        const mockUser = {
          id: "player1",
          getFlag: vi.fn(() => null),
          setFlag: vi.fn().mockResolvedValue(undefined),
        };
        adapter.users.get.mockReturnValue(mockUser);

        const dataURL = "data:image/png;base64,test";
        await expect(
          manager.setSetting("player1", "customPortraitFallback", dataURL)
        ).rejects.toThrow(TypeError);
      });
    });
  });
});
