import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

/**
 * Docker Stack Integration Tests
 * Tests the full Docker Compose stack deployment
 */
describe("Docker Stack Integration Tests", () => {
  const TIMEOUT = 120000; // 2 minutes for Docker operations
  const HEALTH_CHECK_RETRIES = 30;
  const HEALTH_CHECK_INTERVAL = 2000;

  /**
   * Helper function to execute Docker Compose commands
   */
  async function dockerCompose(command: string): Promise<string> {
    const { stdout } = await execAsync(`docker compose ${command}`, {
      cwd: process.cwd(),
    });
    return stdout;
  }

  /**
   * Helper function to check if a service is healthy
   */
  async function waitForService(
    serviceName: string,
    retries = HEALTH_CHECK_RETRIES
  ): Promise<boolean> {
    for (let i = 0; i < retries; i++) {
      try {
        const { stdout } = await execAsync(`docker compose ps --format json ${serviceName}`);
        const serviceInfo = JSON.parse(stdout);

        if (serviceInfo.Health === "healthy" || serviceInfo.State === "running") {
          return true;
        }
      } catch (error) {
        // Service not ready yet
      }

      await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL));
    }
    return false;
  }

  /**
   * Helper function to check HTTP endpoint
   */
  async function checkHttpEndpoint(url: string): Promise<boolean> {
    try {
      const response = await fetch(url);
      return response.ok;
    } catch (error) {
      return false;
    }
  }

  describe("Core Services", () => {
    beforeAll(async () => {
      // Ensure clean state
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    afterAll(async () => {
      // Cleanup
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    it(
      "should start PostgreSQL with health check",
      async () => {
        // Start only PostgreSQL
        await dockerCompose("up -d postgres");

        // Wait for service to be healthy
        const isHealthy = await waitForService("postgres");
        expect(isHealthy).toBe(true);

        // Verify container is running
        const ps = await dockerCompose("ps postgres");
        expect(ps).toContain("mosaic-postgres");
        expect(ps).toContain("Up");
      },
      TIMEOUT
    );

    it(
      "should start Valkey with health check",
      async () => {
        // Start Valkey
        await dockerCompose("up -d valkey");

        // Wait for service to be healthy
        const isHealthy = await waitForService("valkey");
        expect(isHealthy).toBe(true);

        // Verify container is running
        const ps = await dockerCompose("ps valkey");
        expect(ps).toContain("mosaic-valkey");
        expect(ps).toContain("Up");
      },
      TIMEOUT
    );

    it(
      "should have proper network configuration",
      async () => {
        // Check if mosaic-internal network exists
        const { stdout } = await execAsync("docker network ls");
        expect(stdout).toContain("mosaic-internal");
        expect(stdout).toContain("mosaic-public");
      },
      TIMEOUT
    );

    it(
      "should have proper volume configuration",
      async () => {
        // Check if volumes are created
        const { stdout } = await execAsync("docker volume ls");
        expect(stdout).toContain("mosaic-postgres-data");
        expect(stdout).toContain("mosaic-valkey-data");
      },
      TIMEOUT
    );
  });

  describe("Application Services", () => {
    beforeAll(async () => {
      // Start core services first
      await dockerCompose("up -d postgres valkey");
      await waitForService("postgres");
      await waitForService("valkey");
    }, TIMEOUT);

    afterAll(async () => {
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    it(
      "should start API service with dependencies",
      async () => {
        // Start API
        await dockerCompose("up -d api");

        // Wait for API to be healthy
        const isHealthy = await waitForService("api");
        expect(isHealthy).toBe(true);

        // Verify API is accessible
        const apiHealthy = await checkHttpEndpoint("http://localhost:3001/health");
        expect(apiHealthy).toBe(true);
      },
      TIMEOUT
    );

    it(
      "should start Web service with dependencies",
      async () => {
        // Ensure API is running
        await dockerCompose("up -d api");
        await waitForService("api");

        // Start Web
        await dockerCompose("up -d web");

        // Wait for Web to be healthy
        const isHealthy = await waitForService("web");
        expect(isHealthy).toBe(true);

        // Verify Web is accessible
        const webHealthy = await checkHttpEndpoint("http://localhost:3000");
        expect(webHealthy).toBe(true);
      },
      TIMEOUT
    );
  });

  describe("Optional Services (Profiles)", () => {
    afterAll(async () => {
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    it(
      "should start Authentik services with profile",
      async () => {
        // Start Authentik services using profile
        await dockerCompose("--profile authentik up -d");

        // Wait for Authentik dependencies
        await waitForService("authentik-postgres");
        await waitForService("authentik-redis");

        // Verify Authentik server starts
        const isHealthy = await waitForService("authentik-server");
        expect(isHealthy).toBe(true);

        // Verify Authentik is accessible
        const authentikHealthy = await checkHttpEndpoint("http://localhost:9000/-/health/live/");
        expect(authentikHealthy).toBe(true);
      },
      TIMEOUT
    );

    it(
      "should start Ollama service with profile",
      async () => {
        // Start Ollama using profile
        await dockerCompose("--profile ollama up -d ollama");

        // Wait for Ollama to start
        const isHealthy = await waitForService("ollama");
        expect(isHealthy).toBe(true);

        // Verify Ollama is accessible
        const ollamaHealthy = await checkHttpEndpoint("http://localhost:11434/api/tags");
        expect(ollamaHealthy).toBe(true);
      },
      TIMEOUT
    );
  });

  describe("Full Stack", () => {
    it(
      "should start entire stack with all services",
      async () => {
        // Clean slate
        await dockerCompose("down -v").catch(() => {});

        // Start all core services (without profiles)
        await dockerCompose("up -d");

        // Wait for all core services
        const postgresHealthy = await waitForService("postgres");
        const valkeyHealthy = await waitForService("valkey");
        const apiHealthy = await waitForService("api");
        const webHealthy = await waitForService("web");

        expect(postgresHealthy).toBe(true);
        expect(valkeyHealthy).toBe(true);
        expect(apiHealthy).toBe(true);
        expect(webHealthy).toBe(true);

        // Verify all services are running
        const ps = await dockerCompose("ps");
        expect(ps).toContain("mosaic-postgres");
        expect(ps).toContain("mosaic-valkey");
        expect(ps).toContain("mosaic-api");
        expect(ps).toContain("mosaic-web");
      },
      TIMEOUT * 2
    );
  });

  describe("Service Dependencies", () => {
    beforeAll(async () => {
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    afterAll(async () => {
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    it(
      "should enforce dependency order",
      async () => {
        // Start web service (should auto-start dependencies)
        await dockerCompose("up -d web");

        // Wait a bit for services to start
        await new Promise((resolve) => setTimeout(resolve, 10000));

        // Verify all dependencies are running
        const ps = await dockerCompose("ps");
        expect(ps).toContain("mosaic-postgres");
        expect(ps).toContain("mosaic-valkey");
        expect(ps).toContain("mosaic-api");
        expect(ps).toContain("mosaic-web");
      },
      TIMEOUT
    );
  });

  describe("Container Labels", () => {
    beforeAll(async () => {
      await dockerCompose("up -d postgres valkey");
    }, TIMEOUT);

    afterAll(async () => {
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    it("should have proper labels on containers", async () => {
      // Check PostgreSQL labels
      const { stdout: pgLabels } = await execAsync(
        'docker inspect mosaic-postgres --format "{{json .Config.Labels}}"'
      );
      const pgLabelsObj = JSON.parse(pgLabels);
      expect(pgLabelsObj["com.mosaic.service"]).toBe("database");
      expect(pgLabelsObj["com.mosaic.description"]).toBeDefined();

      // Check Valkey labels
      const { stdout: valkeyLabels } = await execAsync(
        'docker inspect mosaic-valkey --format "{{json .Config.Labels}}"'
      );
      const valkeyLabelsObj = JSON.parse(valkeyLabels);
      expect(valkeyLabelsObj["com.mosaic.service"]).toBe("cache");
      expect(valkeyLabelsObj["com.mosaic.description"]).toBeDefined();
    });
  });

  describe("Failure Scenarios", () => {
    const TIMEOUT = 120000;

    beforeAll(async () => {
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    afterAll(async () => {
      await dockerCompose("down -v").catch(() => {});
    }, TIMEOUT);

    it(
      "should handle service restart after crash",
      async () => {
        await dockerCompose("up -d postgres");
        await waitForService("postgres");

        const { stdout: containerName } = await execAsync("docker compose ps -q postgres");
        const trimmedName = containerName.trim();

        await execAsync(`docker kill ${trimmedName}`);

        await new Promise((resolve) => setTimeout(resolve, 3000));

        await dockerCompose("up -d postgres");

        const isHealthy = await waitForService("postgres");
        expect(isHealthy).toBe(true);
      },
      TIMEOUT
    );

    it(
      "should handle port conflict gracefully",
      async () => {
        try {
          const { stdout } = await execAsync(
            "docker run -d -p 5432:5432 --name port-blocker postgres:17-alpine"
          );

          await dockerCompose("up -d postgres").catch((error) => {
            expect(error.message).toContain("port is already allocated");
          });
        } finally {
          await execAsync("docker rm -f port-blocker").catch(() => {});
        }
      },
      TIMEOUT
    );

    it(
      "should handle invalid volume mount paths",
      async () => {
        try {
          await execAsync(
            "docker run -d --name invalid-mount -v /nonexistent/path:/data postgres:17-alpine"
          );

          const { stdout } = await execAsync(
            'docker ps -a -f name=invalid-mount --format "{{.Status}}"'
          );

          expect(stdout).toBeDefined();
        } catch (error) {
          expect(error).toBeDefined();
        } finally {
          await execAsync("docker rm -f invalid-mount").catch(() => {});
        }
      },
      TIMEOUT
    );

    it(
      "should handle network partition scenarios",
      async () => {
        await dockerCompose("up -d postgres valkey");
        await waitForService("postgres");
        await waitForService("valkey");

        const { stdout: postgresContainer } = await execAsync("docker compose ps -q postgres");
        const trimmedPostgres = postgresContainer.trim();

        await execAsync(`docker network disconnect mosaic-internal ${trimmedPostgres}`);

        await new Promise((resolve) => setTimeout(resolve, 2000));

        await execAsync(`docker network connect mosaic-internal ${trimmedPostgres}`);

        const isHealthy = await waitForService("postgres");
        expect(isHealthy).toBe(true);
      },
      TIMEOUT
    );

    it(
      "should handle container out of memory scenario",
      async () => {
        try {
          const { stdout } = await execAsync(
            'docker run -d --name mem-limited --memory="10m" postgres:17-alpine'
          );

          await new Promise((resolve) => setTimeout(resolve, 5000));

          const { stdout: status } = await execAsync(
            'docker inspect mem-limited --format "{{.State.Status}}"'
          );

          expect(["exited", "running"]).toContain(status.trim());
        } finally {
          await execAsync("docker rm -f mem-limited").catch(() => {});
        }
      },
      TIMEOUT
    );

    it(
      "should handle disk space issues gracefully",
      async () => {
        await dockerCompose("up -d postgres");
        await waitForService("postgres");

        const { stdout } = await execAsync("docker system df");
        expect(stdout).toContain("Images");
        expect(stdout).toContain("Containers");
        expect(stdout).toContain("Volumes");
      },
      TIMEOUT
    );

    it(
      "should handle service dependency failures",
      async () => {
        await dockerCompose("up -d postgres").catch(() => {});

        const { stdout: postgresContainer } = await execAsync("docker compose ps -q postgres");
        const trimmedPostgres = postgresContainer.trim();

        if (trimmedPostgres) {
          await execAsync(`docker stop ${trimmedPostgres}`);

          try {
            await dockerCompose("up -d api");
          } catch (error) {
            expect(error).toBeDefined();
          }

          await dockerCompose("start postgres").catch(() => {});
          await waitForService("postgres");

          await dockerCompose("up -d api");
          const apiHealthy = await waitForService("api");
          expect(apiHealthy).toBe(true);
        }
      },
      TIMEOUT
    );

    it(
      "should recover from corrupted volume data",
      async () => {
        await dockerCompose("up -d postgres");
        await waitForService("postgres");

        await dockerCompose("down");

        await execAsync("docker volume rm mosaic-postgres-data").catch(() => {});

        await dockerCompose("up -d postgres");
        const isHealthy = await waitForService("postgres");
        expect(isHealthy).toBe(true);
      },
      TIMEOUT
    );
  });
});
