/**
 * TestDataManager - Manages test data with namespace isolation and automatic cleanup
 *
 * This utility provides:
 * - Unique namespace per test to avoid conflicts in parallel execution
 * - Resource tracking for automatic cleanup
 * - Cleanup in reverse order (newest first) to respect FK constraints
 *
 * @example
 * ```typescript
 * let testData: TestDataManager;
 *
 * test.beforeEach(async ({ request }, testInfo) => {
 *   testData = new TestDataManager(request, testInfo.title);
 * });
 *
 * test.afterEach(async () => {
 *   await testData.cleanup();
 * });
 *
 * test('example', async () => {
 *   const { id, domain } = await testData.createProxyHost({
 *     domain: 'app.example.com',
 *     forwardHost: '192.168.1.100',
 *     forwardPort: 3000
 *   });
 * });
 * ```
 */

import { APIRequestContext } from '@playwright/test';
import * as crypto from 'crypto';

/**
 * Represents a managed resource created during tests
 */
export interface ManagedResource {
  /** Unique identifier of the resource */
  id: string;
  /** Type of resource for cleanup routing */
  type: 'proxy-host' | 'certificate' | 'access-list' | 'dns-provider' | 'user';
  /** Namespace that owns this resource */
  namespace: string;
  /** When the resource was created (for ordering cleanup) */
  createdAt: Date;
}

/**
 * Data required to create a proxy host
 */
export interface ProxyHostData {
  domain: string;
  forwardHost: string;
  forwardPort: number;
  name?: string;
  scheme?: 'http' | 'https';
  websocketSupport?: boolean;
}

/**
 * Data required to create an access list
 */
export interface AccessListData {
  name: string;
  /** Access list type - determines allow/deny behavior */
  type: 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist';
  /** Optional description */
  description?: string;
  /** IP/CIDR rules for whitelist/blacklist types */
  ipRules?: Array<{ cidr: string; description?: string }>;
  /** Comma-separated country codes for geo types */
  countryCodes?: string;
  /** Restrict to local RFC1918 networks */
  localNetworkOnly?: boolean;
  /** Whether the access list is enabled */
  enabled?: boolean;
}

/**
 * Data required to create a certificate
 */
export interface CertificateData {
  domains: string[];
  type: 'letsencrypt' | 'custom';
  privateKey?: string;
  certificate?: string;
}

/**
 * Data required to create a DNS provider
 */
export interface DNSProviderData {
  /** Provider type identifier from backend registry */
  providerType: 'manual' | 'cloudflare' | 'route53' | 'webhook' | 'rfc2136' | string;
  /** Display name for the provider */
  name: string;
  /** Provider-specific credentials */
  credentials: Record<string, string>;
  /** Propagation timeout in seconds */
  propagationTimeout?: number;
  /** Polling interval in seconds */
  pollingInterval?: number;
  /** Whether this is the default provider */
  isDefault?: boolean;
}

/**
 * Data required to create a user
 */
export interface UserData {
  name: string;
  email: string;
  password: string;
  role: 'admin' | 'user' | 'guest';
}

/**
 * Result of creating a proxy host
 */
export interface ProxyHostResult {
  id: string;
  domain: string;
}

/**
 * Result of creating an access list
 */
export interface AccessListResult {
  id: string;
  name: string;
}

/**
 * Result of creating a certificate
 */
export interface CertificateResult {
  id: string;
  domains: string[];
}

/**
 * Result of creating a DNS provider
 */
export interface DNSProviderResult {
  id: string;
  name: string;
}

/**
 * Result of creating a user
 */
export interface UserResult {
  id: string;
  email: string;
  token: string;
}

/**
 * Manages test data lifecycle with namespace isolation
 */
export class TestDataManager {
  private resources: ManagedResource[] = [];
  private namespace: string;
  private request: APIRequestContext;

  /**
   * Creates a new TestDataManager instance
   * @param request - Playwright API request context
   * @param testName - Optional test name for namespace generation
   */
  constructor(request: APIRequestContext, testName?: string) {
    this.request = request;
    // Create unique namespace per test to avoid conflicts
    this.namespace = testName
      ? `test-${this.sanitize(testName)}-${Date.now()}`
      : `test-${crypto.randomUUID()}`;
  }

  /**
   * Sanitizes a test name for use in identifiers
   * Keeps it short to avoid overly long domain names
   */
  private sanitize(name: string): string {
    return name
      .toLowerCase()
      .replace(/[^a-z0-9]/g, '-')
      .replace(/-+/g, '-')  // Collapse multiple dashes
      .substring(0, 15);  // Keep short to avoid long domains
  }

  /**
   * Create a proxy host with automatic cleanup tracking
   * @param data - Proxy host configuration
   * @returns Created proxy host details
   */
  async createProxyHost(data: ProxyHostData): Promise<ProxyHostResult> {
    // Domain already contains uniqueness from generateDomain() in fixtures
    // Only add namespace prefix for test identification/cleanup purposes
    const namespacedDomain = `${this.namespace}.${data.domain}`;

    // Build payload matching backend ProxyHost model field names
    const payload: Record<string, unknown> = {
      domain_names: namespacedDomain,  // API expects domain_names, not domain
      forward_host: data.forwardHost,
      forward_port: data.forwardPort,
      forward_scheme: data.scheme ?? 'http',
      websocket_support: data.websocketSupport ?? false,
      enabled: true,
    };

    // Include name if provided (required for UI edits)
    if (data.name) {
      payload.name = data.name;
    }

    const response = await this.request.post('/api/v1/proxy-hosts', {
      data: payload,
    });

    if (!response.ok()) {
      throw new Error(`Failed to create proxy host: ${await response.text()}`);
    }

    const result = await response.json();
    this.resources.push({
      id: result.uuid || result.id,
      type: 'proxy-host',
      namespace: this.namespace,
      createdAt: new Date(),
    });

    return { id: result.uuid || result.id, domain: namespacedDomain };
  }

  /**
   * Create an access list with automatic cleanup tracking
   * @param data - Access list configuration
   * @returns Created access list details
   */
  async createAccessList(data: AccessListData): Promise<AccessListResult> {
    const namespacedName = `${this.namespace}-${data.name}`;

    // Build payload matching backend AccessList model
    const payload: Record<string, unknown> = {
      name: namespacedName,
      type: data.type,
      description: data.description ?? '',
      enabled: data.enabled ?? true,
      local_network_only: data.localNetworkOnly ?? false,
    };

    // Convert ipRules array to JSON string for ip_rules field
    if (data.ipRules && data.ipRules.length > 0) {
      payload.ip_rules = JSON.stringify(data.ipRules);
    }

    // Add country codes for geo types
    if (data.countryCodes) {
      payload.country_codes = data.countryCodes;
    }

    const response = await this.request.post('/api/v1/access-lists', {
      data: payload,
    });

    if (!response.ok()) {
      throw new Error(`Failed to create access list: ${await response.text()}`);
    }

    const result = await response.json();
    this.resources.push({
      id: result.id?.toString() ?? result.uuid,
      type: 'access-list',
      namespace: this.namespace,
      createdAt: new Date(),
    });

    return { id: result.id?.toString() ?? result.uuid, name: namespacedName };
  }

  /**
   * Create a certificate with automatic cleanup tracking
   * @param data - Certificate configuration
   * @returns Created certificate details
   */
  async createCertificate(data: CertificateData): Promise<CertificateResult> {
    const namespacedDomains = data.domains.map((d) => `${this.namespace}.${d}`);
    const namespaced = {
      ...data,
      domains: namespacedDomains,
    };

    const response = await this.request.post('/api/v1/certificates', {
      data: namespaced,
    });

    if (!response.ok()) {
      throw new Error(`Failed to create certificate: ${await response.text()}`);
    }

    const result = await response.json();
    this.resources.push({
      id: result.id,
      type: 'certificate',
      namespace: this.namespace,
      createdAt: new Date(),
    });

    return { id: result.id, domains: namespacedDomains };
  }

  /**
   * Create a DNS provider with automatic cleanup tracking
   * @param data - DNS provider configuration
   * @returns Created DNS provider details
   */
  async createDNSProvider(data: DNSProviderData): Promise<DNSProviderResult> {
    const namespacedName = `${this.namespace}-${data.name}`;

    // Build payload matching backend CreateDNSProviderRequest struct
    const payload: Record<string, unknown> = {
      name: namespacedName,
      provider_type: data.providerType,
      credentials: data.credentials,
    };

    // Add optional fields if provided
    if (data.propagationTimeout !== undefined) {
      payload.propagation_timeout = data.propagationTimeout;
    }
    if (data.pollingInterval !== undefined) {
      payload.polling_interval = data.pollingInterval;
    }
    if (data.isDefault !== undefined) {
      payload.is_default = data.isDefault;
    }

    const response = await this.request.post('/api/v1/dns-providers', {
      data: payload,
    });

    if (!response.ok()) {
      throw new Error(`Failed to create DNS provider: ${await response.text()}`);
    }

    const result = await response.json();

    // DNS provider IDs must be numeric (backend uses strconv.ParseUint)
    const id = result.id;
    if (id !== undefined && typeof id !== 'number') {
      console.warn(`DNS provider returned non-numeric ID: ${id} (type: ${typeof id}), using as-is`);
    }
    const resourceId = String(id ?? result.uuid);

    this.resources.push({
      id: resourceId,
      type: 'dns-provider',
      namespace: this.namespace,
      createdAt: new Date(),
    });

    return { id: resourceId, name: namespacedName };
  }

  /**
   * Create a test user with automatic cleanup tracking
   * @param data - User configuration
   * @returns Created user details including auth token
   */
  async createUser(data: UserData): Promise<UserResult> {
    const namespacedEmail = `${this.namespace}+${data.email}`;
    const namespaced = {
      name: data.name,
      email: namespacedEmail,
      password: data.password,
      role: data.role,
    };

    const response = await this.request.post('/api/v1/users', {
      data: namespaced,
    });

    if (!response.ok()) {
      throw new Error(`Failed to create user: ${await response.text()}`);
    }

    const result = await response.json();
    this.resources.push({
      id: result.id,
      type: 'user',
      namespace: this.namespace,
      createdAt: new Date(),
    });

    // Automatically log in the user and return token
    const loginResponse = await this.request.post('/api/v1/auth/login', {
      data: { email: namespacedEmail, password: data.password },
    });

    if (!loginResponse.ok()) {
      // User created but login failed - still return user info
      console.warn(`User created but login failed: ${await loginResponse.text()}`);
      return { id: result.id, email: namespacedEmail, token: '' };
    }

    const { token } = await loginResponse.json();

    return { id: result.id, email: namespacedEmail, token };
  }

  /**
   * Clean up all resources in reverse order (respects FK constraints)
   * Resources are deleted newest-first to handle dependencies
   */
  async cleanup(): Promise<void> {
    // Sort by creation time (newest first) to respect dependencies
    const sortedResources = [...this.resources].sort(
      (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
    );

    const errors: Error[] = [];

    for (const resource of sortedResources) {
      try {
        await this.deleteResource(resource);
      } catch (error) {
        errors.push(error as Error);
        console.error(`Failed to cleanup ${resource.type}:${resource.id}:`, error);
      }
    }

    this.resources = [];

    if (errors.length > 0) {
      console.warn(`Cleanup completed with ${errors.length} errors`);
    }
  }

  /**
   * Delete a single managed resource
   */
  private async deleteResource(resource: ManagedResource): Promise<void> {
    const endpoints: Record<ManagedResource['type'], string> = {
      'proxy-host': `/api/v1/proxy-hosts/${resource.id}`,
      certificate: `/api/v1/certificates/${resource.id}`,
      'access-list': `/api/v1/access-lists/${resource.id}`,
      'dns-provider': `/api/v1/dns-providers/${resource.id}`,
      user: `/api/v1/users/${resource.id}`,
    };

    const endpoint = endpoints[resource.type];
    const response = await this.request.delete(endpoint);

    // 404 is acceptable - resource may have been deleted by another test
    if (!response.ok() && response.status() !== 404) {
      const errorText = await response.text();
      // Skip "Cannot delete your own account" errors - the test user is logged in
      // and will be cleaned up when the auth session ends or by admin cleanup
      if (resource.type === 'user' && errorText.includes('Cannot delete your own account')) {
        return; // Silently skip - this is expected for the authenticated test user
      }
      throw new Error(`Failed to delete ${resource.type}: ${errorText}`);
    }
  }

  /**
   * Get all resources created in this namespace
   * @returns Copy of the resources array
   */
  getResources(): ManagedResource[] {
    return [...this.resources];
  }

  /**
   * Get namespace identifier
   * @returns The unique namespace for this test
   */
  getNamespace(): string {
    return this.namespace;
  }

  /**
   * Assert that ACL and rate limiting are disabled before proceeding with ACL-dependent operations.
   * Fails fast with actionable error if security is still enabled.
   * Use this before tests that create/modify resources (proxy hosts, certificates, etc.)
   * to prevent 403 errors from security modules.
   *
   * @throws Error if ACL or rate limiting is enabled
   */
  async assertSecurityDisabled(): Promise<void> {
    try {
      const response = await this.request.get('/api/v1/security/config', { timeout: 3000 });
      if (!response.ok()) {
        // Endpoint might not exist or requires different auth - skip check
        return;
      }

      const config = await response.json();
      const aclEnabled = config.acl?.enabled === true;
      const rateLimitEnabled = config.rateLimit?.enabled === true;

      if (aclEnabled || rateLimitEnabled) {
        throw new Error(
          `\n❌ SECURITY MODULES ARE ENABLED - OPERATION WILL FAIL\n` +
          `   ACL: ${aclEnabled}, Rate Limiting: ${rateLimitEnabled}\n` +
          `   Cannot proceed with resource creation.\n` +
          `   Check: global-setup.ts emergency reset completed successfully\n`
        );
      }
    } catch (error) {
      // Re-throw if it's our security error
      if (error instanceof Error && error.message.includes('SECURITY MODULES ARE ENABLED')) {
        throw error;
      }
      // Otherwise, skip check (endpoint might not exist in this environment)
    }
  }

  /**
   * Force cleanup all test-created resources by pattern matching.
   * This is a nuclear option for cleaning up orphaned test data from previous runs.
   * Use sparingly - prefer the regular cleanup() method.
   *
   * @param request - Playwright API request context
   * @returns Object with counts of deleted resources
   */
  static async forceCleanupAll(request: APIRequestContext): Promise<{
    proxyHosts: number;
    accessLists: number;
    dnsProviders: number;
    certificates: number;
  }> {
    const results = {
      proxyHosts: 0,
      accessLists: 0,
      dnsProviders: 0,
      certificates: 0,
    };

    // Pattern to match test-created resources (namespace starts with 'test-')
    const testPattern = /^test-/;

    try {
      // Clean up proxy hosts
      const hostsResponse = await request.get('/api/v1/proxy-hosts');
      if (hostsResponse.ok()) {
        const hosts = await hostsResponse.json();
        for (const host of hosts) {
          // Match domains that contain test namespace pattern
          if (host.domain && testPattern.test(host.domain)) {
            const deleteResponse = await request.delete(`/api/v1/proxy-hosts/${host.uuid || host.id}`);
            if (deleteResponse.ok() || deleteResponse.status() === 404) {
              results.proxyHosts++;
            }
          }
        }
      }

      // Clean up access lists
      const aclResponse = await request.get('/api/v1/access-lists');
      if (aclResponse.ok()) {
        const acls = await aclResponse.json();
        for (const acl of acls) {
          if (acl.name && testPattern.test(acl.name)) {
            const deleteResponse = await request.delete(`/api/v1/access-lists/${acl.id}`);
            if (deleteResponse.ok() || deleteResponse.status() === 404) {
              results.accessLists++;
            }
          }
        }
      }

      // Clean up DNS providers
      const dnsResponse = await request.get('/api/v1/dns-providers');
      if (dnsResponse.ok()) {
        const providers = await dnsResponse.json();
        for (const provider of providers) {
          if (provider.name && testPattern.test(provider.name)) {
            const deleteResponse = await request.delete(`/api/v1/dns-providers/${provider.id}`);
            if (deleteResponse.ok() || deleteResponse.status() === 404) {
              results.dnsProviders++;
            }
          }
        }
      }

      // Clean up certificates
      const certResponse = await request.get('/api/v1/certificates');
      if (certResponse.ok()) {
        const certs = await certResponse.json();
        for (const cert of certs) {
          // Check if any domain matches test pattern
          const domains = cert.domains || [];
          const isTestCert = domains.some((d: string) => testPattern.test(d));
          if (isTestCert) {
            const deleteResponse = await request.delete(`/api/v1/certificates/${cert.id}`);
            if (deleteResponse.ok() || deleteResponse.status() === 404) {
              results.certificates++;
            }
          }
        }
      }
    } catch (error) {
      console.error('Error during force cleanup:', error);
    }

    console.log(`Force cleanup completed: ${JSON.stringify(results)}`);
    return results;
  }
}
