// SPDX-License-Identifier: MIT

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// Mock createServer to avoid pulling in all tool dependencies
const mockConnect = vi.fn();
const mockServer = { connect: mockConnect };

vi.mock('../server.js', () => ({
  createServer: vi.fn(() => mockServer),
  SERVER_VERSION: '0.0.0-test',
}));

// Mock the MCP SDK transports (must be constructable — no arrow functions)
const mockStdioTransport = {};
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => {
  return {
    StdioServerTransport: vi.fn(function () {
      return mockStdioTransport;
    }),
  };
});

const mockHttpTransport = { handleRequest: vi.fn() };
vi.mock('@modelcontextprotocol/sdk/server/streamableHttp.js', () => {
  return {
    StreamableHTTPServerTransport: vi.fn(function () {
      return mockHttpTransport;
    }),
  };
});

const mockListen = vi.fn((_port: number, _host: string, cb: () => void) => cb());
const mockAll = vi.fn();
const mockUse = vi.fn();
const mockPost = vi.fn();
const mockGet = vi.fn();
const mockSet = vi.fn();
const mockExpressApp = {
  all: mockAll,
  get: mockGet,
  set: mockSet,
  listen: mockListen,
  use: mockUse,
  post: mockPost,
};
vi.mock('@modelcontextprotocol/sdk/server/express.js', () => ({
  createMcpExpressApp: vi.fn(() => mockExpressApp),
}));

// Mock express (needed when auth is enabled)
const mockUrlencoded = vi.fn(() => 'urlencoded-middleware');
vi.mock('express', () => ({
  default: { urlencoded: mockUrlencoded },
}));

// Mock auth SDK modules
const mockMcpAuthMiddleware = vi.fn();
vi.mock('@modelcontextprotocol/sdk/server/auth/router.js', () => ({
  mcpAuthRouter: vi.fn(() => mockMcpAuthMiddleware),
}));

const mockBearerMiddleware = vi.fn();
vi.mock('@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js', () => ({
  requireBearerAuth: vi.fn(() => mockBearerMiddleware),
}));

// Mock ocs fetchStatus to prevent real network calls during startup probe
vi.mock('../client/ocs.js', () => ({
  fetchStatus: vi.fn().mockResolvedValue({ version: '28.0.0' }),
}));

// Mock the auth provider and login handler
const mockProviderInstance = {};
vi.mock('../auth/provider.js', () => ({
  NextcloudOAuthProvider: vi.fn(function () {
    return mockProviderInstance;
  }),
  renderLoginForm: vi.fn(() => '<form></form>'),
}));

const mockLoginHandlerFn = vi.fn();
vi.mock('../auth/login.js', () => ({
  loginHandler: vi.fn(() => mockLoginHandlerFn),
}));

// ---- stdio transport ----

describe('stdio transport', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should create server and connect with StdioServerTransport', async () => {
    const { startStdio } = await import('../transports/stdio.js');
    await startStdio();

    expect(mockConnect).toHaveBeenCalledWith(mockStdioTransport);
  });
});

// ---- http transport (auth disabled) ----

describe('http transport', () => {
  const savedEnv = process.env;

  beforeEach(() => {
    vi.clearAllMocks();
    process.env = { ...savedEnv, MCP_ALLOW_UNAUTHENTICATED: 'true' };
    delete process.env.MCP_AUTH_ENABLED;
  });

  afterEach(() => {
    process.env = savedEnv;
  });

  it('should create server and connect with StreamableHTTPServerTransport', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();

    // Transport is created per-request; invoke the registered handler to trigger connect
    const mcpCall = mockAll.mock.calls.find((c) => c[0] === '/mcp');
    const handler = mcpCall?.[1] as (req: unknown, res: unknown) => Promise<void>;
    expect(handler).toBeDefined();
    await handler({ body: {} }, { on: vi.fn() });

    expect(mockConnect).toHaveBeenCalledWith(mockHttpTransport);
  });

  it('should mount handler on /mcp', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();

    expect(mockAll).toHaveBeenCalledWith('/mcp', expect.any(Function));
  });

  it('should listen on default port 3339', async () => {
    delete process.env.MCP_PORT;
    const { startHttp } = await import('../transports/http.js');
    await startHttp();

    expect(mockListen).toHaveBeenCalledWith(3339, '0.0.0.0', expect.any(Function));
  });

  it('should respect MCP_PORT env var', async () => {
    process.env.MCP_PORT = '4000';
    const { startHttp } = await import('../transports/http.js');
    await startHttp();

    expect(mockListen).toHaveBeenCalledWith(4000, '0.0.0.0', expect.any(Function));
  });

  it('should respect MCP_HOST env var', async () => {
    process.env.MCP_HOST = '127.0.0.1';
    const { startHttp } = await import('../transports/http.js');
    await startHttp();

    expect(mockListen).toHaveBeenCalledWith(expect.any(Number), '127.0.0.1', expect.any(Function));
  });

  it('should not mount auth middleware when MCP_AUTH_ENABLED is unset', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();

    expect(mockPost).not.toHaveBeenCalled();
    // /mcp is mounted with only one handler (no bearer middleware)
    expect(mockAll).toHaveBeenCalledWith('/mcp', expect.any(Function));
  });

  it('registers GET /health endpoint', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();
    expect(mockGet).toHaveBeenCalledWith('/health', expect.any(Function));
  });

  it('/health handler returns 200 {"status":"ok"}', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();
    const healthCall = mockGet.mock.calls.find((c) => c[0] === '/health');
    const handler = healthCall?.[1];
    const res = { status: vi.fn().mockReturnThis(), json: vi.fn() };
    handler({}, res);
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.json).toHaveBeenCalledWith({ status: 'ok' });
  });
});

// ---- http transport (auth enabled) ----

describe('http transport with auth enabled', () => {
  const savedEnv = process.env;

  beforeEach(() => {
    vi.clearAllMocks();
    process.env = {
      ...savedEnv,
      MCP_AUTH_ENABLED: 'true',
      MCP_AUTH_SECRET: 'test-secret-min-32-chars-required!!',
      MCP_AUTH_ISSUER: 'https://mcp.example.com',
    };
  });

  afterEach(() => {
    process.env = savedEnv;
  });

  it('throws when MCP_AUTH_ISSUER is missing', async () => {
    delete process.env.MCP_AUTH_ISSUER;
    const { startHttp } = await import('../transports/http.js');
    await expect(startHttp()).rejects.toThrow('MCP_AUTH_ISSUER');
  });

  it('throws when MCP_AUTH_SECRET is missing', async () => {
    delete process.env.MCP_AUTH_SECRET;
    const { startHttp } = await import('../transports/http.js');
    await expect(startHttp()).rejects.toThrow('MCP_AUTH_SECRET');
  });

  it('mounts the OAuth auth router via app.use', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();
    expect(mockUse).toHaveBeenCalledWith(mockMcpAuthMiddleware);
  });

  it('mounts urlencoded body parser via app.use', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();
    expect(mockUse).toHaveBeenCalledWith('urlencoded-middleware');
  });

  it('registers POST /auth/login handler', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();
    expect(mockPost).toHaveBeenCalledWith('/auth/login', expect.any(Function), mockLoginHandlerFn);
  });

  it('mounts /mcp with bearer middleware and handler', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();
    expect(mockAll).toHaveBeenCalledWith(
      '/mcp',
      expect.any(Function),
      mockBearerMiddleware,
      expect.any(Function)
    );
  });

  it('still connects the MCP server to the transport', async () => {
    const { startHttp } = await import('../transports/http.js');
    await startHttp();

    // Transport is created per-request; invoke the last handler on /mcp to trigger connect
    const mcpCall = mockAll.mock.calls.find((c) => c[0] === '/mcp');
    const handler = mcpCall?.[mcpCall.length - 1] as (req: unknown, res: unknown) => Promise<void>;
    expect(handler).toBeDefined();
    await handler({ body: {} }, { on: vi.fn() });

    expect(mockConnect).toHaveBeenCalledWith(mockHttpTransport);
  });
});
