/**
 * Integration tests for RemoteProjectModule.
 *
 * Tests hook handlers through the Dispatcher -- the same
 * infrastructure used by operations -- with frozen contexts and result/error
 * collection.
 */

import { describe, it, expect } from "vitest";
import type { HookContext, ResolvedHooks, HookResult } from "../intents/lib/operation";
import type { IntentModule } from "../intents/lib/module";

import { SILENT_LOGGER } from "../boundaries/platform/logging";
import {
  createMockGitClient,
  gitClientMatchers,
} from "../boundaries/platform/git-client.state-mock";
import { createMockPathProvider } from "../boundaries/platform/path-provider.test-utils";
import { createFileSystemMock } from "../boundaries/platform/filesystem.state-mock";
import { createRemoteProjectModule } from "./remote-project-module";
import { OPEN_PROJECT_OPERATION_ID } from "../intents/open-project";
import type {
  ResolveHookResult,
  ResolveHookInput,
  OpenProjectIntent,
  CloneProgressReporter,
} from "../intents/open-project";
import { CLOSE_PROJECT_OPERATION_ID } from "../intents/close-project";
import type { CloseHookInput, CloseHookResult, CloseProjectIntent } from "../intents/close-project";
import { Path } from "../utils/path/path";
import { extractRepoName } from "../utils/url-utils";
import type { ProjectId } from "../shared/api/types";

// Pre-computed: generateProjectIdFromUrl("https://github.com/org/repo.git")
const URL_PROJECT_ID = "repo-4c06e3f1" as ProjectId;

expect.extend(gitClientMatchers);

const noopReport: CloneProgressReporter = () => {};

// =============================================================================
// Test Setup
// =============================================================================

/**
 * Build a ResolvedHooks from a module's hook declarations for a given operation.
 */
function resolveHooksFromModule(module: IntentModule, operationId: string): ResolvedHooks {
  const opHooks = module.hooks?.[operationId] ?? {};
  return {
    collect: async <T>(hookPointId: string, ctx: HookContext): Promise<HookResult<T>> => {
      const hookHandler = opHooks[hookPointId];
      if (!hookHandler) {
        return { results: [], errors: [], capabilities: {} };
      }
      try {
        const result = await hookHandler.handler(ctx);
        // Filter out undefined results to match Dispatcher behavior (undefined = "not handled")
        const results = result !== undefined ? [result as T] : [];
        return { results, errors: [], capabilities: {} };
      } catch (error) {
        return { results: [], errors: [error as Error], capabilities: {} };
      }
    },
  };
}

function createTestSetup() {
  const fs = createFileSystemMock();
  const gitClient = createMockGitClient();
  const pathProvider = createMockPathProvider();

  const module = createRemoteProjectModule({
    fs,
    gitClient,
    pathProvider,
    logger: SILENT_LOGGER,
  });

  const hookRegistry = {
    resolve: (operationId: string) => resolveHooksFromModule(module, operationId),
  };

  return { hookRegistry, fs, gitClient, pathProvider };
}

// =============================================================================
// Intent Helpers
// =============================================================================

function openProjectIntent(payload: { git?: string; path?: Path }): OpenProjectIntent {
  return {
    type: "project:open",
    payload,
  };
}

function resolveContext(
  intent: OpenProjectIntent,
  report: CloneProgressReporter = noopReport
): ResolveHookInput {
  return { intent, report };
}

function closeProjectIntent(payload: {
  projectPath: string;
  removeLocalRepo?: boolean;
}): CloseProjectIntent {
  return {
    type: "project:close",
    payload: payload as CloseProjectIntent["payload"],
  };
}

// =============================================================================
// Tests
// =============================================================================

describe("RemoteProjectModule Integration", () => {
  // ---------------------------------------------------------------------------
  // open-project / resolve
  // ---------------------------------------------------------------------------

  describe("open-project / resolve", () => {
    it("clones new repo when URL provided and no existing clone", async () => {
      const { hookRegistry, gitClient } = createTestSetup();

      const hooks = hookRegistry.resolve(OPEN_PROJECT_OPERATION_ID);
      const intent = openProjectIntent({ git: "https://github.com/org/repo.git" });

      const { results, errors } = await hooks.collect<ResolveHookResult | undefined>(
        "resolve",
        resolveContext(intent)
      );

      expect(errors).toHaveLength(0);
      expect(results).toHaveLength(1);

      const result = results[0]!;
      expect(result).toBeDefined();
      const projectPath = result.projectPath;
      expect(projectPath).toContain("repo");
      expect(result.remoteUrl).toBe("https://github.com/org/repo.git");

      // Verify clone was called
      expect(gitClient).toHaveClonedRepository(projectPath!);
    });

    it("returns existing path when clone directory exists (readdir succeeds)", async () => {
      const { hookRegistry, fs, gitClient, pathProvider } = createTestSetup();

      const url = "https://github.com/org/repo.git";
      const repoName = extractRepoName(url);
      const projectDir = new Path(pathProvider.dataPath("remotes"), URL_PROJECT_ID);
      const gitPath = new Path(projectDir.toString(), repoName);

      // Pre-populate filesystem so readdir succeeds
      fs.$.setEntry(gitPath.toString(), { type: "directory" });

      const hooks = hookRegistry.resolve(OPEN_PROJECT_OPERATION_ID);
      const intent = openProjectIntent({ git: url });

      const { results, errors } = await hooks.collect<ResolveHookResult | undefined>(
        "resolve",
        resolveContext(intent)
      );

      expect(errors).toHaveLength(0);
      expect(results).toHaveLength(1);

      const result = results[0]!;
      expect(result).toBeDefined();
      expect(result!.projectPath).toBe(gitPath.toString());
      expect(result!.remoteUrl).toBe(url);

      // No clone should have happened
      expect(gitClient.$.repositories.size).toBe(0);
    });

    it("returns undefined for local path (no git URL)", async () => {
      const { hookRegistry } = createTestSetup();

      const hooks = hookRegistry.resolve(OPEN_PROJECT_OPERATION_ID);
      const intent = openProjectIntent({ path: new Path("/local/project") });

      const { results, errors } = await hooks.collect<ResolveHookResult | undefined>(
        "resolve",
        resolveContext(intent)
      );

      expect(errors).toHaveLength(0);
      expect(results).toHaveLength(0);
    });

    it("propagates clone error", async () => {
      const { hookRegistry, gitClient } = createTestSetup();

      const originalClone = gitClient.clone.bind(gitClient);
      (gitClient as { clone: typeof gitClient.clone }).clone = async () => {
        throw new Error("Network error: connection refused");
      };

      const hooks = hookRegistry.resolve(OPEN_PROJECT_OPERATION_ID);
      const intent = openProjectIntent({ git: "https://github.com/org/repo.git" });

      const { results, errors } = await hooks.collect<ResolveHookResult | undefined>(
        "resolve",
        resolveContext(intent)
      );

      expect(errors).toHaveLength(1);
      expect(errors[0]!.message).toBe("Network error: connection refused");
      expect(results).toHaveLength(0);

      // Restore
      (gitClient as { clone: typeof gitClient.clone }).clone = originalClone;
    });

    it("reports clone progress via report callback", async () => {
      const { hookRegistry, gitClient } = createTestSetup();

      const progressEvents: Array<{ stage: string; progress: number; name: string }> = [];
      const report: CloneProgressReporter = (stage, progress, name) => {
        progressEvents.push({ stage, progress, name });
      };

      // Override mock clone to invoke onProgress
      const originalClone = gitClient.clone.bind(gitClient);
      (gitClient as { clone: typeof gitClient.clone }).clone = async (
        url: string,
        targetPath: Path,
        onProgress?: (event: { stage: string; progress: number }) => void
      ) => {
        onProgress?.({ stage: "receiving", progress: 50 });
        onProgress?.({ stage: "resolving", progress: 100 });
        return originalClone(url, targetPath);
      };

      const hooks = hookRegistry.resolve(OPEN_PROJECT_OPERATION_ID);
      const intent = openProjectIntent({ git: "https://github.com/org/repo.git" });

      await hooks.collect<ResolveHookResult | undefined>("resolve", resolveContext(intent, report));

      expect(progressEvents).toEqual([
        { stage: "receiving", progress: 0.5, name: "repo" },
        { stage: "resolving", progress: 1, name: "repo" },
      ]);

      // Restore
      (gitClient as { clone: typeof gitClient.clone }).clone = originalClone;
    });
  });

  // ---------------------------------------------------------------------------
  // close-project / close
  // ---------------------------------------------------------------------------

  describe("close-project / close", () => {
    it("deletes clone directory when removeLocalRepo=true and remoteUrl in context", async () => {
      const { hookRegistry, fs } = createTestSetup();

      const projectPath = "/test/app-data/remotes/abc12345/repo";

      // Pre-populate clone directory
      fs.$.setEntry("/test/app-data/remotes/abc12345", { type: "directory" });
      fs.$.setEntry(projectPath, { type: "directory" });

      const closeHooks = hookRegistry.resolve(CLOSE_PROJECT_OPERATION_ID);
      const closeIntnt = closeProjectIntent({
        projectPath: "/test/project",
        removeLocalRepo: true,
      });

      const closeCtx: CloseHookInput = {
        intent: closeIntnt,
        projectPath,
        removeLocalRepo: true,
        remoteUrl: "https://github.com/org/repo.git",
      };
      const { results, errors } = await closeHooks.collect<CloseHookResult>("close", closeCtx);

      expect(errors).toHaveLength(0);
      expect(results).toHaveLength(1);

      // Clone dir (parent of projectPath) should be deleted
      expect(fs.$.entries.has(new Path("/test/app-data/remotes/abc12345").toString())).toBe(false);
    });

    it("no-op when removeLocalRepo=false", async () => {
      const { hookRegistry, fs } = createTestSetup();

      const projectPath = "/test/app-data/remotes/abc12345/repo";
      fs.$.setEntry("/test/app-data/remotes/abc12345", { type: "directory" });
      fs.$.setEntry(projectPath, { type: "directory" });

      const closeHooks = hookRegistry.resolve(CLOSE_PROJECT_OPERATION_ID);
      const closeIntnt = closeProjectIntent({
        projectPath: "/test/project",
        removeLocalRepo: false,
      });

      const closeCtx: CloseHookInput = {
        intent: closeIntnt,
        projectPath,
        removeLocalRepo: false,
        remoteUrl: "https://github.com/org/repo.git",
      };
      const { results, errors } = await closeHooks.collect<CloseHookResult>("close", closeCtx);

      expect(errors).toHaveLength(0);
      expect(results).toHaveLength(1);
      expect(results[0]).toEqual({});

      // Directory should still exist
      expect(fs.$.entries.has(new Path("/test/app-data/remotes/abc12345").toString())).toBe(true);
    });

    it("no-op when no remoteUrl in context (local project)", async () => {
      const { hookRegistry } = createTestSetup();

      const projectPath = "/home/user/projects/local";

      const closeHooks = hookRegistry.resolve(CLOSE_PROJECT_OPERATION_ID);
      const closeIntnt = closeProjectIntent({
        projectPath: "/test/project",
        removeLocalRepo: true,
      });

      const closeCtx: CloseHookInput = {
        intent: closeIntnt,
        projectPath,
        removeLocalRepo: true,
        // No remoteUrl — local project
      };
      const { results, errors } = await closeHooks.collect<CloseHookResult>("close", closeCtx);

      expect(errors).toHaveLength(0);
      expect(results).toHaveLength(1);
      expect(results[0]).toEqual({});
    });
  });
});
