import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createCreateCommand } from '../../src/commands/create.js';
import { type ApiClient, ApiError } 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 () => ({})),
    delete: vi.fn(async () => {}),
  } as unknown as ApiClient;
}

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

  beforeEach(() => {
    client = mockClient();
    output = [];
  });

  describe('create server', () => {
    it('creates a server with minimal flags', async () => {
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['server', 'my-server'], { from: 'user' });
      expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({
        name: 'my-server',
        transport: 'STDIO',
        replicas: 1,
      }));
      expect(output.join('\n')).toContain("server 'test' created");
    });

    it('creates a server with all flags', async () => {
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'server', 'ha-mcp',
        '-d', 'Home Assistant MCP',
        '--docker-image', 'ghcr.io/ha-mcp:latest',
        '--transport', 'STREAMABLE_HTTP',
        '--external-url', 'http://localhost:8086/mcp',
        '--container-port', '3000',
        '--replicas', '2',
        '--command', 'python',
        '--command', '-c',
        '--command', 'print("hello")',
        '--env', 'API_KEY=secretRef:creds:API_KEY',
        '--env', 'BASE_URL=http://localhost',
      ], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/servers', {
        name: 'ha-mcp',
        description: 'Home Assistant MCP',
        dockerImage: 'ghcr.io/ha-mcp:latest',
        transport: 'STREAMABLE_HTTP',
        externalUrl: 'http://localhost:8086/mcp',
        containerPort: 3000,
        replicas: 2,
        command: ['python', '-c', 'print("hello")'],
        env: [
          { name: 'API_KEY', valueFrom: { secretRef: { name: 'creds', key: 'API_KEY' } } },
          { name: 'BASE_URL', value: 'http://localhost' },
        ],
      });
    });

    it('defaults transport to STDIO', async () => {
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['server', 'test'], { from: 'user' });
      expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({
        transport: 'STDIO',
      }));
    });

    it('strips null values from template when using --from-template', async () => {
      vi.mocked(client.get).mockResolvedValueOnce([{
        id: 'tpl-1',
        name: 'grafana',
        version: '1.0.0',
        description: 'Grafana MCP',
        packageName: '@leval/mcp-grafana',
        dockerImage: null,
        transport: 'STDIO',
        repositoryUrl: 'https://github.com/test',
        externalUrl: null,
        command: null,
        containerPort: null,
        replicas: 1,
        env: [{ name: 'TOKEN', required: true, description: 'A token' }],
        healthCheck: { tool: 'test', arguments: {} },
        createdAt: '2025-01-01',
        updatedAt: '2025-01-01',
      }] as never);
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'server', 'my-grafana', '--from-template=grafana',
        '--env', 'TOKEN=secretRef:creds:TOKEN',
      ], { from: 'user' });
      const call = vi.mocked(client.post).mock.calls[0]![1] as Record<string, unknown>;
      // null fields from template should NOT be in the body
      expect(call).not.toHaveProperty('dockerImage');
      expect(call).not.toHaveProperty('externalUrl');
      expect(call).not.toHaveProperty('command');
      expect(call).not.toHaveProperty('containerPort');
      // non-null fields should be present
      expect(call.packageName).toBe('@leval/mcp-grafana');
      expect(call.healthCheck).toEqual({ tool: 'test', arguments: {} });
      expect(call.templateName).toBe('grafana');
    });

    it('throws on 409 without --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Server already exists: my-server"}'));
      const cmd = createCreateCommand({ client, log });
      await expect(cmd.parseAsync(['server', 'my-server'], { from: 'user' })).rejects.toThrow('API error 409');
    });

    it('updates existing server on 409 with --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Server already exists"}'));
      vi.mocked(client.get).mockResolvedValueOnce([{ id: 'srv-1', name: 'my-server' }] as never);
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['server', 'my-server', '--force'], { from: 'user' });
      expect(client.put).toHaveBeenCalledWith('/api/v1/servers/srv-1', expect.objectContaining({
        transport: 'STDIO',
      }));
      expect(output.join('\n')).toContain("server 'my-server' updated");
    });
  });

  describe('create secret', () => {
    it('creates a secret with --data flags', async () => {
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'secret', 'ha-creds',
        '--data', 'TOKEN=abc123',
        '--data', 'URL=https://ha.local',
      ], { from: 'user' });
      expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', {
        name: 'ha-creds',
        data: { TOKEN: 'abc123', URL: 'https://ha.local' },
      });
      expect(output.join('\n')).toContain("secret 'test' created");
    });

    it('creates a secret with empty data', async () => {
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['secret', 'empty-secret'], { from: 'user' });
      expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', {
        name: 'empty-secret',
        data: {},
      });
    });

    it('throws on 409 without --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Secret already exists: my-creds"}'));
      const cmd = createCreateCommand({ client, log });
      await expect(cmd.parseAsync(['secret', 'my-creds', '--data', 'KEY=val'], { from: 'user' })).rejects.toThrow('API error 409');
    });

    it('updates existing secret on 409 with --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Secret already exists"}'));
      vi.mocked(client.get).mockResolvedValueOnce([{ id: 'sec-1', name: 'my-creds' }] as never);
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['secret', 'my-creds', '--data', 'KEY=val', '--force'], { from: 'user' });
      expect(client.put).toHaveBeenCalledWith('/api/v1/secrets/sec-1', { data: { KEY: 'val' } });
      expect(output.join('\n')).toContain("secret 'my-creds' updated");
    });
  });

  describe('create project', () => {
    it('creates a project', async () => {
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['project', 'my-project', '-d', 'A test project'], { from: 'user' });
      expect(client.post).toHaveBeenCalledWith('/api/v1/projects', {
        name: 'my-project',
        description: 'A test project',
      });
      expect(output.join('\n')).toContain("project 'test' created");
    });

    it('creates a project with no description', async () => {
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['project', 'minimal'], { from: 'user' });
      expect(client.post).toHaveBeenCalledWith('/api/v1/projects', {
        name: 'minimal',
        description: '',
      });
    });

    it('updates existing project on 409 with --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Project already exists"}'));
      vi.mocked(client.get).mockResolvedValueOnce([{ id: 'proj-1', name: 'my-proj' }] as never);
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['project', 'my-proj', '-d', 'updated', '--force'], { from: 'user' });
      expect(client.put).toHaveBeenCalledWith('/api/v1/projects/proj-1', { description: 'updated' });
      expect(output.join('\n')).toContain("project 'my-proj' updated");
    });
  });

  describe('create user', () => {
    it('creates a user with password and name', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'usr-1', email: 'alice@test.com' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'user', 'alice@test.com',
        '--password', 'secret123',
        '--name', 'Alice',
      ], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/users', {
        email: 'alice@test.com',
        password: 'secret123',
        name: 'Alice',
      });
      expect(output.join('\n')).toContain("user 'alice@test.com' created");
    });

    it('does not send role field (RBAC is the auth mechanism)', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'usr-1', email: 'admin@test.com' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'user', 'admin@test.com',
        '--password', 'pass123',
      ], { from: 'user' });

      const callBody = vi.mocked(client.post).mock.calls[0]![1] as Record<string, unknown>;
      expect(callBody).not.toHaveProperty('role');
    });

    it('requires --password', async () => {
      const cmd = createCreateCommand({ client, log });
      await expect(cmd.parseAsync(['user', 'alice@test.com'], { from: 'user' })).rejects.toThrow('--password is required');
    });

    it('throws on 409 without --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"User already exists"}'));
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['user', 'alice@test.com', '--password', 'pass'], { from: 'user' }),
      ).rejects.toThrow('API error 409');
    });

    it('updates existing user on 409 with --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"User already exists"}'));
      vi.mocked(client.get).mockResolvedValueOnce([{ id: 'usr-1', email: 'alice@test.com' }] as never);
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'user', 'alice@test.com', '--password', 'newpass', '--name', 'Alice New', '--force',
      ], { from: 'user' });

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

  describe('create group', () => {
    it('creates a group with members', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'grp-1', name: 'dev-team' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'group', 'dev-team',
        '--description', 'Development team',
        '--member', 'alice@test.com',
        '--member', 'bob@test.com',
      ], { from: 'user' });

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

    it('creates a group with no members', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'grp-1', name: 'empty-group' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['group', 'empty-group'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/groups', {
        name: 'empty-group',
        members: [],
      });
    });

    it('throws on 409 without --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Group already exists"}'));
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['group', 'dev-team'], { from: 'user' }),
      ).rejects.toThrow('API error 409');
    });

    it('updates existing group on 409 with --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Group already exists"}'));
      vi.mocked(client.get).mockResolvedValueOnce([{ id: 'grp-1', name: 'dev-team' }] as never);
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'group', 'dev-team', '--member', 'new@test.com', '--force',
      ], { from: 'user' });

      expect(client.put).toHaveBeenCalledWith('/api/v1/groups/grp-1', {
        members: ['new@test.com'],
      });
      expect(output.join('\n')).toContain("group 'dev-team' updated");
    });
  });

  describe('create rbac', () => {
    it('creates an RBAC definition with subjects and bindings', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'developers' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'rbac', 'developers',
        '--subject', 'User:alice@test.com',
        '--subject', 'Group:dev-team',
        '--binding', 'edit:servers',
        '--binding', 'view:instances',
      ], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
        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("rbac 'developers' created");
    });

    it('creates an RBAC definition with wildcard resource', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'admins' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'rbac', 'admins',
        '--subject', 'User:admin@test.com',
        '--binding', 'edit:*',
      ], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
        name: 'admins',
        subjects: [{ kind: 'User', name: 'admin@test.com' }],
        roleBindings: [{ role: 'edit', resource: '*' }],
      });
    });

    it('creates an RBAC definition with empty subjects and bindings', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'empty' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['rbac', 'empty'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
        name: 'empty',
        subjects: [],
        roleBindings: [],
      });
    });

    it('throws on invalid subject format', async () => {
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['rbac', 'bad', '--subject', 'no-colon'], { from: 'user' }),
      ).rejects.toThrow('Invalid subject format');
    });

    it('throws on invalid binding format', async () => {
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['rbac', 'bad', '--binding', 'no-colon'], { from: 'user' }),
      ).rejects.toThrow('Invalid binding format');
    });

    it('throws on 409 without --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"RBAC already exists"}'));
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['rbac', 'developers', '--subject', 'User:a@b.com', '--binding', 'edit:servers'], { from: 'user' }),
      ).rejects.toThrow('API error 409');
    });

    it('updates existing RBAC on 409 with --force', async () => {
      vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"RBAC already exists"}'));
      vi.mocked(client.get).mockResolvedValueOnce([{ id: 'rbac-1', name: 'developers' }] as never);
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'rbac', 'developers',
        '--subject', 'User:new@test.com',
        '--binding', 'edit:*',
        '--force',
      ], { from: 'user' });

      expect(client.put).toHaveBeenCalledWith('/api/v1/rbac/rbac-1', {
        subjects: [{ kind: 'User', name: 'new@test.com' }],
        roleBindings: [{ role: 'edit', resource: '*' }],
      });
      expect(output.join('\n')).toContain("rbac 'developers' updated");
    });

    it('creates an RBAC definition with operation bindings', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'ops' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'rbac', 'ops',
        '--subject', 'Group:ops-team',
        '--binding', 'edit:servers',
        '--operation', 'logs',
        '--operation', 'backup',
      ], { from: 'user' });

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

    it('creates an RBAC definition with name-scoped binding', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'ha-viewer' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync([
        'rbac', 'ha-viewer',
        '--subject', 'User:alice@test.com',
        '--binding', 'view:servers:my-ha',
      ], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
        name: 'ha-viewer',
        subjects: [{ kind: 'User', name: 'alice@test.com' }],
        roleBindings: [
          { role: 'view', resource: 'servers', name: 'my-ha' },
        ],
      });
    });
  });

  describe('create prompt', () => {
    it('creates a prompt with content', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'test-prompt' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['prompt', 'test-prompt', '--content', 'Hello world'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', {
        name: 'test-prompt',
        content: 'Hello world',
      });
      expect(output.join('\n')).toContain("prompt 'test-prompt' created");
    });

    it('requires content or content-file', async () => {
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['prompt', 'no-content'], { from: 'user' }),
      ).rejects.toThrow('--content or --content-file is required');
    });

    it('--priority sets prompt priority', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'pri-prompt' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['prompt', 'pri-prompt', '--content', 'x', '--priority', '8'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
        priority: 8,
      }));
    });

    it('--priority validates range 1-10', async () => {
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--priority', '15'], { from: 'user' }),
      ).rejects.toThrow('--priority must be a number between 1 and 10');
    });

    it('--priority rejects zero', async () => {
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--priority', '0'], { from: 'user' }),
      ).rejects.toThrow('--priority must be a number between 1 and 10');
    });

    it('--link sets linkTarget', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'linked' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['prompt', 'linked', '--content', 'x', '--link', 'proj/srv:docmost://pages/abc'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
        linkTarget: 'proj/srv:docmost://pages/abc',
      }));
    });

    it('--project resolves project name to ID', async () => {
      vi.mocked(client.get).mockResolvedValueOnce([{ id: 'proj-1', name: 'my-project' }] as never);
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'scoped' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['prompt', 'scoped', '--content', 'x', '--project', 'my-project'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
        projectId: 'proj-1',
      }));
    });

    it('--project throws when project not found', async () => {
      vi.mocked(client.get).mockResolvedValueOnce([] as never);
      const cmd = createCreateCommand({ client, log });
      await expect(
        cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--project', 'nope'], { from: 'user' }),
      ).rejects.toThrow("Project 'nope' not found");
    });
  });

  describe('create promptrequest', () => {
    it('creates a prompt request with priority', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'r-1', name: 'req' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['promptrequest', 'req', '--content', 'proposal', '--priority', '7'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/promptrequests', expect.objectContaining({
        name: 'req',
        content: 'proposal',
        priority: 7,
      }));
    });
  });

  describe('create project', () => {
    it('creates a project with --gated', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'proj-1', name: 'gated-proj' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['project', 'gated-proj', '--gated'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
        gated: true,
      }));
    });

    it('creates a project with --no-gated', async () => {
      vi.mocked(client.post).mockResolvedValueOnce({ id: 'proj-1', name: 'open-proj' });
      const cmd = createCreateCommand({ client, log });
      await cmd.parseAsync(['project', 'open-proj', '--no-gated'], { from: 'user' });

      expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
        gated: false,
      }));
    });
  });
});
