import { Test, TestingModule } from "@nestjs/testing";
import { BridgeModule } from "./bridge.module";
import { DiscordService } from "./discord/discord.service";
import { MatrixService } from "./matrix/matrix.service";
import { StitcherService } from "../stitcher/stitcher.service";
import { PrismaService } from "../prisma/prisma.service";
import { BullMqService } from "../bullmq/bullmq.service";
import { ChatProxyService } from "../chat-proxy/chat-proxy.service";
import { CHAT_PROVIDERS } from "./bridge.constants";
import type { IChatProvider } from "./interfaces";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";

// Mock discord.js
const mockReadyCallbacks: Array<() => void> = [];
const mockClient = {
  login: vi.fn().mockImplementation(async () => {
    mockReadyCallbacks.forEach((cb) => cb());
    return Promise.resolve();
  }),
  destroy: vi.fn().mockResolvedValue(undefined),
  on: vi.fn(),
  once: vi.fn().mockImplementation((event: string, callback: () => void) => {
    if (event === "ready") {
      mockReadyCallbacks.push(callback);
    }
  }),
  user: { tag: "TestBot#1234" },
  channels: {
    fetch: vi.fn(),
  },
  guilds: {
    fetch: vi.fn(),
  },
};

vi.mock("discord.js", () => {
  return {
    Client: class MockClient {
      login = mockClient.login;
      destroy = mockClient.destroy;
      on = mockClient.on;
      once = mockClient.once;
      user = mockClient.user;
      channels = mockClient.channels;
      guilds = mockClient.guilds;
    },
    Events: {
      ClientReady: "ready",
      MessageCreate: "messageCreate",
      Error: "error",
    },
    GatewayIntentBits: {
      Guilds: 1 << 0,
      GuildMessages: 1 << 9,
      MessageContent: 1 << 15,
    },
  };
});

// Mock matrix-bot-sdk
vi.mock("matrix-bot-sdk", () => {
  return {
    MatrixClient: class MockMatrixClient {
      start = vi.fn().mockResolvedValue(undefined);
      stop = vi.fn();
      on = vi.fn();
      sendMessage = vi.fn().mockResolvedValue("$mock-event-id");
    },
    SimpleFsStorageProvider: class MockStorage {
      constructor(_path: string) {
        // no-op
      }
    },
    AutojoinRoomsMixin: {
      setupOnClient: vi.fn(),
    },
  };
});

/**
 * Saved environment variables to restore after each test
 */
interface SavedEnvVars {
  DISCORD_BOT_TOKEN?: string;
  DISCORD_GUILD_ID?: string;
  DISCORD_CONTROL_CHANNEL_ID?: string;
  MATRIX_ACCESS_TOKEN?: string;
  MATRIX_HOMESERVER_URL?: string;
  MATRIX_BOT_USER_ID?: string;
  MATRIX_CONTROL_ROOM_ID?: string;
  MATRIX_WORKSPACE_ID?: string;
  ENCRYPTION_KEY?: string;
  MOSAIC_SECRET_KEY?: string;
}

describe("BridgeModule", () => {
  let savedEnv: SavedEnvVars;

  beforeEach(() => {
    // Save current env vars
    savedEnv = {
      DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN,
      DISCORD_GUILD_ID: process.env.DISCORD_GUILD_ID,
      DISCORD_CONTROL_CHANNEL_ID: process.env.DISCORD_CONTROL_CHANNEL_ID,
      MATRIX_ACCESS_TOKEN: process.env.MATRIX_ACCESS_TOKEN,
      MATRIX_HOMESERVER_URL: process.env.MATRIX_HOMESERVER_URL,
      MATRIX_BOT_USER_ID: process.env.MATRIX_BOT_USER_ID,
      MATRIX_CONTROL_ROOM_ID: process.env.MATRIX_CONTROL_ROOM_ID,
      MATRIX_WORKSPACE_ID: process.env.MATRIX_WORKSPACE_ID,
      ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
      MOSAIC_SECRET_KEY: process.env.MOSAIC_SECRET_KEY,
    };

    // Clear all bridge env vars
    delete process.env.DISCORD_BOT_TOKEN;
    delete process.env.DISCORD_GUILD_ID;
    delete process.env.DISCORD_CONTROL_CHANNEL_ID;
    delete process.env.MATRIX_ACCESS_TOKEN;
    delete process.env.MATRIX_HOMESERVER_URL;
    delete process.env.MATRIX_BOT_USER_ID;
    delete process.env.MATRIX_CONTROL_ROOM_ID;
    delete process.env.MATRIX_WORKSPACE_ID;

    // Set encryption key (needed by StitcherService)
    process.env.ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
    // Set MOSAIC_SECRET_KEY (needed by CryptoService via ChatProxyModule)
    process.env.MOSAIC_SECRET_KEY = "test-mosaic-secret-key-minimum-32-characters-long";

    // Clear ready callbacks
    mockReadyCallbacks.length = 0;

    vi.clearAllMocks();
  });

  afterEach(() => {
    // Restore env vars
    for (const [key, value] of Object.entries(savedEnv)) {
      if (value === undefined) {
        delete process.env[key];
      } else {
        process.env[key] = value;
      }
    }
  });

  /**
   * Helper to compile a test module with BridgeModule
   */
  async function compileModule(): Promise<TestingModule> {
    return Test.createTestingModule({
      imports: [BridgeModule],
    })
      .overrideProvider(PrismaService)
      .useValue({})
      .overrideProvider(BullMqService)
      .useValue({})
      .overrideProvider(ChatProxyService)
      .useValue({
        proxyChat: vi.fn().mockResolvedValue(new Response()),
      })
      .compile();
  }

  /**
   * Helper to set Discord env vars
   */
  function setDiscordEnv(): void {
    process.env.DISCORD_BOT_TOKEN = "test-discord-token";
    process.env.DISCORD_GUILD_ID = "test-guild-id";
    process.env.DISCORD_CONTROL_CHANNEL_ID = "test-channel-id";
  }

  /**
   * Helper to set Matrix env vars
   */
  function setMatrixEnv(): void {
    process.env.MATRIX_ACCESS_TOKEN = "test-matrix-token";
    process.env.MATRIX_HOMESERVER_URL = "https://matrix.example.com";
    process.env.MATRIX_BOT_USER_ID = "@bot:example.com";
    process.env.MATRIX_CONTROL_ROOM_ID = "!room:example.com";
    process.env.MATRIX_WORKSPACE_ID = "test-workspace-id";
  }

  describe("with both Discord and Matrix configured", () => {
    let module: TestingModule;

    beforeEach(async () => {
      setDiscordEnv();
      setMatrixEnv();
      module = await compileModule();
    });

    it("should compile the module", () => {
      expect(module).toBeDefined();
    });

    it("should provide DiscordService", () => {
      const discordService = module.get<DiscordService>(DiscordService);
      expect(discordService).toBeDefined();
      expect(discordService).toBeInstanceOf(DiscordService);
    });

    it("should provide MatrixService", () => {
      const matrixService = module.get<MatrixService>(MatrixService);
      expect(matrixService).toBeDefined();
      expect(matrixService).toBeInstanceOf(MatrixService);
    });

    it("should provide CHAT_PROVIDERS with both providers", () => {
      const chatProviders = module.get<IChatProvider[]>(CHAT_PROVIDERS);
      expect(chatProviders).toBeDefined();
      expect(chatProviders).toHaveLength(2);
      expect(chatProviders[0]).toBeInstanceOf(DiscordService);
      expect(chatProviders[1]).toBeInstanceOf(MatrixService);
    });

    it("should provide StitcherService via StitcherModule", () => {
      const stitcherService = module.get<StitcherService>(StitcherService);
      expect(stitcherService).toBeDefined();
      expect(stitcherService).toBeInstanceOf(StitcherService);
    });
  });

  describe("with only Discord configured", () => {
    let module: TestingModule;

    beforeEach(async () => {
      setDiscordEnv();
      module = await compileModule();
    });

    it("should compile the module", () => {
      expect(module).toBeDefined();
    });

    it("should provide DiscordService", () => {
      const discordService = module.get<DiscordService>(DiscordService);
      expect(discordService).toBeDefined();
      expect(discordService).toBeInstanceOf(DiscordService);
    });

    it("should provide CHAT_PROVIDERS with only Discord", () => {
      const chatProviders = module.get<IChatProvider[]>(CHAT_PROVIDERS);
      expect(chatProviders).toBeDefined();
      expect(chatProviders).toHaveLength(1);
      expect(chatProviders[0]).toBeInstanceOf(DiscordService);
    });
  });

  describe("with only Matrix configured", () => {
    let module: TestingModule;

    beforeEach(async () => {
      setMatrixEnv();
      module = await compileModule();
    });

    it("should compile the module", () => {
      expect(module).toBeDefined();
    });

    it("should provide MatrixService", () => {
      const matrixService = module.get<MatrixService>(MatrixService);
      expect(matrixService).toBeDefined();
      expect(matrixService).toBeInstanceOf(MatrixService);
    });

    it("should provide CHAT_PROVIDERS with only Matrix", () => {
      const chatProviders = module.get<IChatProvider[]>(CHAT_PROVIDERS);
      expect(chatProviders).toBeDefined();
      expect(chatProviders).toHaveLength(1);
      expect(chatProviders[0]).toBeInstanceOf(MatrixService);
    });
  });

  describe("with neither bridge configured", () => {
    let module: TestingModule;

    beforeEach(async () => {
      // No env vars set for either bridge
      module = await compileModule();
    });

    it("should compile the module without errors", () => {
      expect(module).toBeDefined();
    });

    it("should provide CHAT_PROVIDERS as an empty array", () => {
      const chatProviders = module.get<IChatProvider[]>(CHAT_PROVIDERS);
      expect(chatProviders).toBeDefined();
      expect(chatProviders).toHaveLength(0);
      expect(Array.isArray(chatProviders)).toBe(true);
    });
  });

  describe("CHAT_PROVIDERS token", () => {
    it("should be a string constant", () => {
      expect(CHAT_PROVIDERS).toBe("CHAT_PROVIDERS");
      expect(typeof CHAT_PROVIDERS).toBe("string");
    });
  });
});
