import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// ---------------------------------------------------------------------------
// Import module under test
// We import from the index barrel to test the public API
// ---------------------------------------------------------------------------
const {
  getAvailablePlugins,
  getPluginSource,
  getPluginVersion,
  getMarketplaceName,
  hasPlugin,
  validateMarketplace,
  discoverUnregisteredPlugins,
  syncMarketplace,
} = await import("./index.js");

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeMarketplaceJson(overrides: Record<string, unknown> = {}): string {
  return JSON.stringify({
    name: "specweave",
    owner: { name: "Test Author", email: "test@example.com" },
    plugins: [
      { name: "sw", source: "./plugins/specweave", version: "1.0.225" },
      { name: "frontend", source: "./plugins/frontend", version: "1.0.0" },
    ],
    metadata: { version: "1.0.225" },
    ...overrides,
  });
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("marketplace.json parser", () => {
  // TC-005: Parse marketplace.json returns plugin entries
  describe("TC-005: getAvailablePlugins", () => {
    it("returns array of plugin entries from valid marketplace.json", () => {
      const content = makeMarketplaceJson();
      const plugins = getAvailablePlugins(content);

      expect(plugins).toHaveLength(2);
      expect(plugins[0]).toEqual({
        name: "sw",
        source: "./plugins/specweave",
        version: "1.0.225",
      });
      expect(plugins[1]).toEqual({
        name: "frontend",
        source: "./plugins/frontend",
        version: "1.0.0",
      });
    });

    it("returns each plugin with name, source, and version fields", () => {
      const content = makeMarketplaceJson();
      const plugins = getAvailablePlugins(content);

      for (const plugin of plugins) {
        expect(plugin).toHaveProperty("name");
        expect(plugin).toHaveProperty("source");
        expect(plugin).toHaveProperty("version");
        expect(typeof plugin.name).toBe("string");
        expect(typeof plugin.source).toBe("string");
        expect(typeof plugin.version).toBe("string");
      }
    });

    it("returns empty array for JSON with no plugins", () => {
      const content = JSON.stringify({ name: "empty", version: "1.0.0", plugins: [] });
      const plugins = getAvailablePlugins(content);

      expect(plugins).toEqual([]);
    });

    it("returns empty array for invalid JSON", () => {
      const plugins = getAvailablePlugins("not valid json {{{");

      expect(plugins).toEqual([]);
    });
  });

  // TC-006: Plugin source mapping resolves paths
  describe("TC-006: getPluginSource", () => {
    it("returns source path for a known plugin name", () => {
      const content = makeMarketplaceJson();
      const source = getPluginSource("frontend", content);

      expect(source).toBe("./plugins/frontend");
    });

    it("returns source path for 'sw' plugin", () => {
      const content = makeMarketplaceJson();
      const source = getPluginSource("sw", content);

      expect(source).toBe("./plugins/specweave");
    });
  });

  // TC-007: Unknown plugin returns null
  describe("TC-007: getPluginSource with unknown plugin", () => {
    it("returns null for a plugin name not in marketplace.json", () => {
      const content = makeMarketplaceJson();
      const source = getPluginSource("sw-nonexistent", content);

      expect(source).toBeNull();
    });

    it("returns null for invalid JSON content", () => {
      const source = getPluginSource("sw", "broken json");

      expect(source).toBeNull();
    });
  });

  // Bonus: getPluginVersion
  describe("getPluginVersion", () => {
    it("returns version for a known plugin", () => {
      const content = makeMarketplaceJson();
      const version = getPluginVersion("frontend", content);

      expect(version).toBe("1.0.0");
    });

    it("returns null for unknown plugin", () => {
      const content = makeMarketplaceJson();
      const version = getPluginVersion("sw-nonexistent", content);

      expect(version).toBeNull();
    });
  });

  // ---------------------------------------------------------------------------
  // getMarketplaceName
  // ---------------------------------------------------------------------------
  describe("getMarketplaceName", () => {
    it("returns the top-level name from valid marketplace.json", () => {
      const content = makeMarketplaceJson();
      expect(getMarketplaceName(content)).toBe("specweave");
    });

    it("returns null for invalid JSON", () => {
      expect(getMarketplaceName("not json")).toBeNull();
    });

    it("returns null when name field is missing", () => {
      expect(getMarketplaceName('{"plugins": []}')).toBeNull();
    });

    it("returns null when name field is empty string", () => {
      expect(getMarketplaceName('{"name": "", "plugins": []}')).toBeNull();
    });
  });

  // ---------------------------------------------------------------------------
  // hasPlugin
  // ---------------------------------------------------------------------------
  describe("hasPlugin", () => {
    it("returns true when plugin exists in manifest", () => {
      const content = makeMarketplaceJson();
      expect(hasPlugin("sw", content)).toBe(true);
    });

    it("returns false when plugin does not exist", () => {
      const content = makeMarketplaceJson();
      expect(hasPlugin("nonexistent", content)).toBe(false);
    });

    it("returns false for invalid JSON", () => {
      expect(hasPlugin("sw", "broken json")).toBe(false);
    });
  });

  // ---------------------------------------------------------------------------
  // validateMarketplace
  // ---------------------------------------------------------------------------
  describe("validateMarketplace", () => {
    it("returns valid for well-formed marketplace.json", () => {
      const content = makeMarketplaceJson();
      const result = validateMarketplace(content);
      expect(result.valid).toBe(true);
      expect(result.pluginCount).toBe(2);
      expect(result.name).toBe("specweave");
      expect(result.error).toBeUndefined();
    });

    it("returns invalid for missing name field", () => {
      const content = JSON.stringify({ plugins: [{ name: "a", source: "./a" }] });
      const result = validateMarketplace(content);
      expect(result.valid).toBe(false);
      expect(result.error).toContain("name");
    });

    it("returns invalid for missing plugins array", () => {
      const content = JSON.stringify({ name: "test" });
      const result = validateMarketplace(content);
      expect(result.valid).toBe(false);
      expect(result.error).toContain("plugins");
    });

    it("returns invalid for empty plugins array", () => {
      const content = JSON.stringify({ name: "test", plugins: [] });
      const result = validateMarketplace(content);
      expect(result.valid).toBe(false);
      expect(result.error).toContain("No plugins");
      expect(result.name).toBe("test");
    });

    it("returns invalid for plugin entry missing source", () => {
      const content = JSON.stringify({ name: "test", plugins: [{ name: "a" }] });
      const result = validateMarketplace(content);
      expect(result.valid).toBe(false);
      expect(result.error).toContain("missing name or source");
    });

    it("returns invalid for plugin entry missing name", () => {
      const content = JSON.stringify({ name: "test", plugins: [{ source: "./a" }] });
      const result = validateMarketplace(content);
      expect(result.valid).toBe(false);
      expect(result.error).toContain("missing name or source");
    });

    it("returns invalid for broken JSON", () => {
      const result = validateMarketplace("not json {{{");
      expect(result.valid).toBe(false);
      expect(result.error).toContain("Invalid JSON");
      expect(result.pluginCount).toBe(0);
    });
  });
});

// ---------------------------------------------------------------------------
// discoverUnregisteredPlugins
// ---------------------------------------------------------------------------

describe("discoverUnregisteredPlugins", () => {
  const originalFetch = globalThis.fetch;

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  it("returns unregistered directories correctly", async () => {
    const manifest = makeMarketplaceJson(); // has "sw" (source: ./plugins/specweave) and "frontend"

    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => [
        { name: "specweave", type: "dir" },  // registered (source dir matches)
        { name: "frontend", type: "dir" },   // registered by name
        { name: "marketing", type: "dir" },  // unregistered
        { name: "backend", type: "dir" },    // unregistered
      ],
    }) as unknown as typeof fetch;

    const result = await discoverUnregisteredPlugins("owner", "repo", manifest);

    expect(result.failed).toBe(false);
    expect(result.plugins).toEqual([
      { name: "marketing", source: "./plugins/marketing" },
      { name: "backend", source: "./plugins/backend" },
    ]);
  });

  it("returns empty plugins and failed=true on API error", async () => {
    const manifest = makeMarketplaceJson();

    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 403,
    }) as unknown as typeof fetch;

    const result = await discoverUnregisteredPlugins("owner", "repo", manifest);

    expect(result.failed).toBe(true);
    expect(result.plugins).toEqual([]);
  });

  it("ignores files, only returns directories", async () => {
    const manifest = makeMarketplaceJson();

    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => [
        { name: "new-plugin", type: "dir" },
        { name: "README.md", type: "file" },
        { name: ".gitkeep", type: "file" },
      ],
    }) as unknown as typeof fetch;

    const result = await discoverUnregisteredPlugins("owner", "repo", manifest);

    expect(result.failed).toBe(false);
    expect(result.plugins).toEqual([
      { name: "new-plugin", source: "./plugins/new-plugin" },
    ]);
  });

  it("returns empty plugins and failed=true on network error", async () => {
    const manifest = makeMarketplaceJson();

    globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error")) as unknown as typeof fetch;

    const result = await discoverUnregisteredPlugins("owner", "repo", manifest);

    expect(result.failed).toBe(true);
    expect(result.plugins).toEqual([]);
  });
});

// ---------------------------------------------------------------------------
// syncMarketplace
// ---------------------------------------------------------------------------

describe("syncMarketplace", () => {
  it("adds a plugin not present in manifest", () => {
    const manifest = makeMarketplaceJson(); // has "sw" and "frontend"
    const result = syncMarketplace(manifest, [
      { name: "newplugin", description: "New plugin", version: "1.0.0" },
    ]);

    expect(result.added).toEqual(["newplugin"]);
    expect(result.updated).toEqual([]);
    expect(result.unchanged).toEqual([]);
    const names = result.updatedManifest.plugins.map((p) => p.name);
    expect(names).toContain("newplugin");
    const entry = result.updatedManifest.plugins.find((p) => p.name === "newplugin");
    expect(entry?.source).toBe("./plugins/newplugin");
    expect(entry?.version).toBe("1.0.0");
    expect(entry?.description).toBe("New plugin");
  });

  it("updates an existing entry when version drifts", () => {
    const manifest = makeMarketplaceJson(); // "sw" has version "1.0.225"
    const result = syncMarketplace(manifest, [
      { name: "sw", version: "2.0.0" },
    ]);

    expect(result.updated).toEqual(["sw"]);
    expect(result.added).toEqual([]);
    const entry = result.updatedManifest.plugins.find((p) => p.name === "sw");
    expect(entry?.version).toBe("2.0.0");
  });

  it("updates an existing entry when description drifts", () => {
    const manifest = makeMarketplaceJson();
    const result = syncMarketplace(manifest, [
      { name: "frontend", description: "New description for frontend" },
    ]);

    expect(result.updated).toEqual(["frontend"]);
    expect(result.added).toEqual([]);
    const entry = result.updatedManifest.plugins.find((p) => p.name === "frontend");
    expect(entry?.description).toBe("New description for frontend");
  });

  it("marks entry unchanged when version and description match", () => {
    const manifest = JSON.stringify({
      name: "vskill",
      owner: { name: "Test" },
      plugins: [
        { name: "mobile", source: "./plugins/mobile", version: "2.3.0", description: "Mobile dev" },
      ],
    });
    const result = syncMarketplace(manifest, [
      { name: "mobile", version: "2.3.0", description: "Mobile dev" },
    ]);

    expect(result.unchanged).toEqual(["mobile"]);
    expect(result.added).toEqual([]);
    expect(result.updated).toEqual([]);
  });

  it("handles empty localPlugins array — all existing plugins are unreported (empty result)", () => {
    const manifest = makeMarketplaceJson();
    const result = syncMarketplace(manifest, []);

    expect(result.added).toEqual([]);
    expect(result.updated).toEqual([]);
    expect(result.unchanged).toEqual([]);
    // existing plugins in manifest are untouched
    expect(result.updatedManifest.plugins).toHaveLength(2);
  });
});
