import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import http from 'node:http';
import type { AddressInfo } from 'node:net';
import { createLoginCommand, createLogoutCommand, defaultStatusRequest } from '../../src/commands/auth.js';
import { saveCredentials, loadCredentials } from '../../src/auth/index.js';
import { saveConfig, DEFAULT_CONFIG } from '../../src/config/index.js';

let tempDir: string;
let output: string[];

function log(...args: string[]) {
  output.push(args.join(' '));
}

beforeEach(() => {
  tempDir = mkdtempSync(join(tmpdir(), 'mcpctl-auth-cmd-test-'));
  output = [];
});

afterEach(() => {
  rmSync(tempDir, { recursive: true, force: true });
});

describe('login command', () => {
  it('stores credentials on successful login', async () => {
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        input: async () => 'alice@test.com',
        password: async () => 'secret123',
      },
      log,
      loginRequest: async (_url, email, _password) => ({
        token: 'session-token-123',
        user: { email },
      }),
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync([], { from: 'user' });
    expect(output[0]).toContain('Logged in as alice@test.com');

    const creds = loadCredentials({ configDir: tempDir });
    expect(creds).not.toBeNull();
    expect(creds!.token).toBe('session-token-123');
    expect(creds!.user).toBe('alice@test.com');
  });

  it('shows error on failed login', async () => {
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        input: async () => 'alice@test.com',
        password: async () => 'wrong',
      },
      log,
      loginRequest: async () => { throw new Error('Invalid credentials'); },
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync([], { from: 'user' });
    expect(output[0]).toContain('Login failed');
    expect(output[0]).toContain('Invalid credentials');

    const creds = loadCredentials({ configDir: tempDir });
    expect(creds).toBeNull();
  });

  it('uses mcpdUrl from config', async () => {
    saveConfig({ ...DEFAULT_CONFIG, mcpdUrl: 'http://custom:3100' }, { configDir: tempDir });
    let capturedUrl = '';
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        input: async () => 'user@test.com',
        password: async () => 'pass',
      },
      log,
      loginRequest: async (url, email) => {
        capturedUrl = url;
        return { token: 'tok', user: { email } };
      },
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync([], { from: 'user' });
    expect(capturedUrl).toBe('http://custom:3100');
  });

  it('suggests the previously-used email when a session for the same mcpd exists', async () => {
    saveConfig({ ...DEFAULT_CONFIG, mcpdUrl: 'http://custom:3100' }, { configDir: tempDir });
    saveCredentials({ token: 'stale', mcpdUrl: 'http://custom:3100', user: 'returning@test.com' }, { configDir: tempDir });
    let suggestedDefault: string | undefined = 'UNSET';
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        // Simulate the user pressing Enter: return the suggested default unchanged.
        input: async (_msg, def) => { suggestedDefault = def; return def ?? ''; },
        password: async () => 'newpass',
      },
      log,
      loginRequest: async (_url, email) => ({ token: 'fresh', user: { email } }),
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync([], { from: 'user' });

    expect(suggestedDefault).toBe('returning@test.com');
    const creds = loadCredentials({ configDir: tempDir });
    expect(creds!.user).toBe('returning@test.com');
  });

  it('does not suggest an email from a different mcpd', async () => {
    saveConfig({ ...DEFAULT_CONFIG, mcpdUrl: 'http://server-a:3100' }, { configDir: tempDir });
    saveCredentials({ token: 'stale', mcpdUrl: 'http://server-b:3100', user: 'elsewhere@test.com' }, { configDir: tempDir });
    let suggestedDefault: string | undefined = 'UNSET';
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        input: async (_msg, def) => { suggestedDefault = def; return 'typed@test.com'; },
        password: async () => 'pass',
      },
      log,
      loginRequest: async (_url, email) => ({ token: 'tok', user: { email } }),
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync([], { from: 'user' });

    expect(suggestedDefault).toBeUndefined();
  });

  it('allows --mcpd-url flag override', async () => {
    let capturedUrl = '';
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        input: async () => 'user@test.com',
        password: async () => 'pass',
      },
      log,
      loginRequest: async (url, email) => {
        capturedUrl = url;
        return { token: 'tok', user: { email } };
      },
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync(['--mcpd-url', 'http://override:3100'], { from: 'user' });
    expect(capturedUrl).toBe('http://override:3100');
  });
});

describe('login bootstrap flow', () => {
  it('bootstraps first admin when no users exist', async () => {
    let bootstrapCalled = false;
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        input: async (msg) => {
          if (msg.includes('Name')) return 'Admin User';
          return 'admin@test.com';
        },
        password: async () => 'admin-pass',
      },
      log,
      loginRequest: async () => ({ token: '', user: { email: '' } }),
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: false }),
      bootstrapRequest: async (_url, email, _password) => {
        bootstrapCalled = true;
        return { token: 'admin-token', user: { email } };
      },
    });
    await cmd.parseAsync([], { from: 'user' });

    expect(bootstrapCalled).toBe(true);
    expect(output.join('\n')).toContain('No users configured');
    expect(output.join('\n')).toContain('admin@test.com');
    expect(output.join('\n')).toContain('admin');

    const creds = loadCredentials({ configDir: tempDir });
    expect(creds).not.toBeNull();
    expect(creds!.token).toBe('admin-token');
    expect(creds!.user).toBe('admin@test.com');
  });

  it('falls back to normal login when users exist', async () => {
    let loginCalled = false;
    const cmd = createLoginCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: {
        input: async () => 'alice@test.com',
        password: async () => 'secret',
      },
      log,
      loginRequest: async (_url, email) => {
        loginCalled = true;
        return { token: 'session-tok', user: { email } };
      },
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => { throw new Error('Should not be called'); },
    });
    await cmd.parseAsync([], { from: 'user' });

    expect(loginCalled).toBe(true);
    expect(output.join('\n')).not.toContain('No users configured');
  });
});

// Regression: `mcpctl login` crashed with "JSON Parse error: Unexpected EOF"
// when mcpdUrl used https. defaultStatusRequest hardcoded the plain-http driver
// and port 80, so an https mcpdUrl hit the ingress on :80, got a redirect with an
// empty body (status < 400), and JSON.parse('') threw a raw SyntaxError.
describe('defaultStatusRequest (real HTTP)', () => {
  let server: http.Server;
  let baseUrl: string;
  let handler: (req: http.IncomingMessage, res: http.ServerResponse) => void;

  beforeEach(async () => {
    handler = (_req, res) => { res.statusCode = 200; res.end(JSON.stringify({ hasUsers: true })); };
    server = http.createServer((req, res) => handler(req, res));
    await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
    const { port } = server.address() as AddressInfo;
    baseUrl = `http://127.0.0.1:${port}`;
  });

  afterEach(async () => {
    await new Promise<void>((resolve) => server.close(() => resolve()));
  });

  it('parses a valid JSON status response', async () => {
    const status = await defaultStatusRequest(baseUrl);
    expect(status.hasUsers).toBe(true);
  });

  it('rejects with a clear error (not a raw SyntaxError) on an empty body', async () => {
    handler = (_req, res) => { res.statusCode = 200; res.end(''); };
    await expect(defaultStatusRequest(baseUrl)).rejects.toThrow(/Invalid status response from mcpd.*empty body/);
  });

  it('rejects with a clear error on a non-JSON (HTML redirect) body', async () => {
    handler = (_req, res) => { res.statusCode = 200; res.end('<html>301 Moved</html>'); };
    await expect(defaultStatusRequest(baseUrl)).rejects.toThrow(/Invalid status response from mcpd/);
  });

  it('uses the TLS driver for an https URL (would silently succeed over plain http before the fix)', async () => {
    const { port } = server.address() as AddressInfo;
    // Point an https URL at the plain-http test server. The https driver cannot
    // complete a TLS handshake with it, so this must fail to connect. The old
    // code always used the http driver and would have resolved instead.
    await expect(defaultStatusRequest(`https://127.0.0.1:${port}`)).rejects.toThrow(/Cannot reach mcpd/);
  });
});

describe('logout command', () => {
  it('removes credentials on logout', async () => {
    saveCredentials({ token: 'tok', mcpdUrl: 'http://x:3100', user: 'alice' }, { configDir: tempDir });
    let logoutCalled = false;
    const cmd = createLogoutCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: { input: async () => '', password: async () => '' },
      log,
      loginRequest: async () => ({ token: '', user: { email: '' } }),
      logoutRequest: async () => { logoutCalled = true; },
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync([], { from: 'user' });
    expect(output[0]).toContain('Logged out successfully');
    expect(logoutCalled).toBe(true);

    const creds = loadCredentials({ configDir: tempDir });
    expect(creds).toBeNull();
  });

  it('shows not logged in when no credentials', async () => {
    const cmd = createLogoutCommand({
      configDeps: { configDir: tempDir },
      credentialsDeps: { configDir: tempDir },
      prompt: { input: async () => '', password: async () => '' },
      log,
      loginRequest: async () => ({ token: '', user: { email: '' } }),
      logoutRequest: async () => {},
      statusRequest: async () => ({ hasUsers: true }),
      bootstrapRequest: async () => ({ token: '', user: { email: '' } }),
    });
    await cmd.parseAsync([], { from: 'user' });
    expect(output[0]).toContain('Not logged in');
  });
});
