import { describe, it, expect, vi, beforeEach } from 'vitest';
import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createApplyCommand } from '../../src/commands/apply.js';
import type { ApiClient } from '../../src/api-client.js';

function mockClient(): ApiClient {
  return {
    get: vi.fn(async () => []),
    post: vi.fn(async () => ({ id: 'new-id', name: 'test' })),
    put: vi.fn(async () => ({ id: 'existing-id', name: 'test' })),
    delete: vi.fn(async () => {}),
  } as unknown as ApiClient;
}

describe('apply command', () => {
  let client: ReturnType<typeof mockClient>;
  let output: string[];
  let tmpDir: string;
  const log = (...args: unknown[]) => output.push(args.map(String).join(' '));

  beforeEach(() => {
    client = mockClient();
    output = [];
    tmpDir = mkdtempSync(join(tmpdir(), 'mcpctl-test-'));
  });

  it('applies servers from YAML file', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
servers:
  - name: slack
    description: Slack MCP server
    transport: STDIO
    packageName: "@anthropic/slack-mcp"
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({ name: 'slack' }));
    expect(output.join('\n')).toContain('Created server: slack');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies servers from JSON file', async () => {
    const configPath = join(tmpDir, 'config.json');
    writeFileSync(configPath, JSON.stringify({
      servers: [{ name: 'github', transport: 'STDIO' }],
    }));

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({ name: 'github' }));
    expect(output.join('\n')).toContain('Created server: github');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('updates existing servers', async () => {
    vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'slack' }]);

    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
servers:
  - name: slack
    description: Updated description
    transport: STDIO
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.put).toHaveBeenCalledWith('/api/v1/servers/srv-1', expect.objectContaining({ name: 'slack' }));
    expect(output.join('\n')).toContain('Updated server: slack');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('supports dry-run mode', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
servers:
  - name: test
    transport: STDIO
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath, '--dry-run'], { from: 'user' });

    expect(client.post).not.toHaveBeenCalled();
    expect(output.join('\n')).toContain('Dry run');
    expect(output.join('\n')).toContain('1 server(s)');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies secrets', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
secrets:
  - name: ha-creds
    data:
      TOKEN: abc123
      URL: https://ha.local
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', expect.objectContaining({
      name: 'ha-creds',
      data: { TOKEN: 'abc123', URL: 'https://ha.local' },
    }));
    expect(output.join('\n')).toContain('Created secret: ha-creds');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('updates existing secrets', async () => {
    vi.mocked(client.get).mockImplementation(async (url: string) => {
      if (url === '/api/v1/secrets') return [{ id: 'sec-1', name: 'ha-creds' }];
      return [];
    });

    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
secrets:
  - name: ha-creds
    data:
      TOKEN: new-token
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.put).toHaveBeenCalledWith('/api/v1/secrets/sec-1', { data: { TOKEN: 'new-token' } });
    expect(output.join('\n')).toContain('Updated secret: ha-creds');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies projects', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
projects:
  - name: my-project
    description: A test project
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({ name: 'my-project' }));
    expect(output.join('\n')).toContain('Created project: my-project');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies users (no role field)', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
users:
  - email: alice@test.com
    password: password123
    name: Alice
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    const callBody = vi.mocked(client.post).mock.calls[0]![1] as Record<string, unknown>;
    expect(callBody).toEqual(expect.objectContaining({
      email: 'alice@test.com',
      password: 'password123',
      name: 'Alice',
    }));
    expect(callBody).not.toHaveProperty('role');
    expect(output.join('\n')).toContain('Created user: alice@test.com');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('updates existing users matched by email', async () => {
    vi.mocked(client.get).mockImplementation(async (url: string) => {
      if (url === '/api/v1/users') return [{ id: 'usr-1', email: 'alice@test.com' }];
      return [];
    });

    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
users:
  - email: alice@test.com
    password: newpassword
    name: Alice Updated
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.put).toHaveBeenCalledWith('/api/v1/users/usr-1', expect.objectContaining({
      email: 'alice@test.com',
      name: 'Alice Updated',
    }));
    expect(output.join('\n')).toContain('Updated user: alice@test.com');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies groups', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
groups:
  - name: dev-team
    description: Development team
    members:
      - alice@test.com
      - bob@test.com
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/groups', expect.objectContaining({
      name: 'dev-team',
      description: 'Development team',
      members: ['alice@test.com', 'bob@test.com'],
    }));
    expect(output.join('\n')).toContain('Created group: dev-team');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('updates existing groups', async () => {
    vi.mocked(client.get).mockImplementation(async (url: string) => {
      if (url === '/api/v1/groups') return [{ id: 'grp-1', name: 'dev-team' }];
      return [];
    });

    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
groups:
  - name: dev-team
    description: Updated devs
    members:
      - new@test.com
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.put).toHaveBeenCalledWith('/api/v1/groups/grp-1', expect.objectContaining({
      name: 'dev-team',
      description: 'Updated devs',
    }));
    expect(output.join('\n')).toContain('Updated group: dev-team');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies rbacBindings', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
rbac:
  - name: developers
    subjects:
      - kind: User
        name: alice@test.com
      - kind: Group
        name: dev-team
    roleBindings:
      - role: edit
        resource: servers
      - role: view
        resource: instances
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', expect.objectContaining({
      name: 'developers',
      subjects: [
        { kind: 'User', name: 'alice@test.com' },
        { kind: 'Group', name: 'dev-team' },
      ],
      roleBindings: [
        { role: 'edit', resource: 'servers' },
        { role: 'view', resource: 'instances' },
      ],
    }));
    expect(output.join('\n')).toContain('Created rbacBinding: developers');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('updates existing rbacBindings', async () => {
    vi.mocked(client.get).mockImplementation(async (url: string) => {
      if (url === '/api/v1/rbac') return [{ id: 'rbac-1', name: 'developers' }];
      return [];
    });

    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
rbacBindings:
  - name: developers
    subjects:
      - kind: User
        name: new@test.com
    roleBindings:
      - role: edit
        resource: "*"
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.put).toHaveBeenCalledWith('/api/v1/rbac/rbac-1', expect.objectContaining({
      name: 'developers',
    }));
    expect(output.join('\n')).toContain('Updated rbacBinding: developers');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies projects with servers', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
projects:
  - name: smart-home
    description: Home automation
    llmProvider: gemini-cli
    llmModel: gemini-2.0-flash
    servers:
      - my-grafana
      - my-ha
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
      name: 'smart-home',
      llmProvider: 'gemini-cli',
      llmModel: 'gemini-2.0-flash',
      servers: ['my-grafana', 'my-ha'],
    }));
    expect(output.join('\n')).toContain('Created project: smart-home');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('dry-run shows all new resource types', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
secrets:
  - name: creds
    data:
      TOKEN: abc
users:
  - email: alice@test.com
    password: password123
groups:
  - name: dev-team
    members: []
projects:
  - name: my-proj
    description: A project
rbacBindings:
  - name: admins
    subjects:
      - kind: User
        name: admin@test.com
    roleBindings:
      - role: edit
        resource: "*"
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath, '--dry-run'], { from: 'user' });

    expect(client.post).not.toHaveBeenCalled();
    const text = output.join('\n');
    expect(text).toContain('Dry run');
    expect(text).toContain('1 secret(s)');
    expect(text).toContain('1 user(s)');
    expect(text).toContain('1 group(s)');
    expect(text).toContain('1 project(s)');
    expect(text).toContain('1 rbacBinding(s)');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies resources in correct order', async () => {
    const callOrder: string[] = [];
    vi.mocked(client.post).mockImplementation(async (url: string) => {
      callOrder.push(url);
      return { id: 'new-id', name: 'test' };
    });

    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
rbacBindings:
  - name: admins
    subjects:
      - kind: User
        name: admin@test.com
    roleBindings:
      - role: edit
        resource: "*"
users:
  - email: admin@test.com
    password: password123
secrets:
  - name: creds
    data:
      KEY: val
groups:
  - name: dev-team
servers:
  - name: my-server
    transport: STDIO
projects:
  - name: my-proj
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    // Apply order: secrets → servers → users → groups → projects → templates → rbacBindings
    expect(callOrder[0]).toBe('/api/v1/secrets');
    expect(callOrder[1]).toBe('/api/v1/servers');
    expect(callOrder[2]).toBe('/api/v1/users');
    expect(callOrder[3]).toBe('/api/v1/groups');
    expect(callOrder[4]).toBe('/api/v1/projects');
    expect(callOrder[5]).toBe('/api/v1/rbac');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies rbac with operation bindings', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
rbac:
  - name: ops-team
    subjects:
      - kind: Group
        name: ops
    roleBindings:
      - role: edit
        resource: servers
      - role: run
        action: backup
      - role: run
        action: logs
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', expect.objectContaining({
      name: 'ops-team',
      roleBindings: [
        { role: 'edit', resource: 'servers' },
        { role: 'run', action: 'backup' },
        { role: 'run', action: 'logs' },
      ],
    }));
    expect(output.join('\n')).toContain('Created rbacBinding: ops-team');

    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('applies rbac with name-scoped resource binding', async () => {
    const configPath = join(tmpDir, 'config.yaml');
    writeFileSync(configPath, `
rbac:
  - name: ha-viewer
    subjects:
      - kind: User
        name: alice@test.com
    roleBindings:
      - role: view
        resource: servers
        name: my-ha
`);

    const cmd = createApplyCommand({ client, log });
    await cmd.parseAsync([configPath], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', expect.objectContaining({
      name: 'ha-viewer',
      roleBindings: [
        { role: 'view', resource: 'servers', name: 'my-ha' },
      ],
    }));

    rmSync(tmpDir, { recursive: true, force: true });
  });
});
