/**
 * Integration tests for the Linear tracker plugin.
 *
 * Requires one of:
 *   - LINEAR_API_KEY (direct Linear API access), or
 *   - COMPOSIO_API_KEY (via Composio SDK, optionally COMPOSIO_ENTITY_ID)
 * Plus:
 *   - LINEAR_TEAM_ID (team to create test issues in)
 *
 * When using Composio, cleanup (issue deletion) still requires LINEAR_API_KEY
 * since that uses a direct GraphQL call outside the plugin.
 *
 * Skipped automatically when prerequisites are missing.
 *
 * Each test run creates a real Linear issue, exercises the plugin methods
 * against it, and deletes it in cleanup. This validates that our GraphQL
 * queries, state mapping, and data parsing work against the real API —
 * not just against mocked responses.
 */

import { request } from "node:https";
import type { ProjectConfig } from "@composio/ao-core";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import trackerLinear from "@composio/ao-plugin-tracker-linear";
import { pollUntilEqual } from "./helpers/polling.js";

// ---------------------------------------------------------------------------
// Prerequisites
// ---------------------------------------------------------------------------

const LINEAR_API_KEY = process.env["LINEAR_API_KEY"];
const COMPOSIO_API_KEY = process.env["COMPOSIO_API_KEY"];
const LINEAR_TEAM_ID = process.env["LINEAR_TEAM_ID"];
const hasCredentials = Boolean(LINEAR_API_KEY || COMPOSIO_API_KEY);
const canRun = hasCredentials && Boolean(LINEAR_TEAM_ID);

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

/**
 * Direct GraphQL call for test setup/cleanup.
 * Only available when LINEAR_API_KEY is set.
 */
function linearGraphQL<T>(query: string, variables: Record<string, unknown>): Promise<T> {
  if (!LINEAR_API_KEY) {
    throw new Error("linearGraphQL requires LINEAR_API_KEY");
  }
  const body = JSON.stringify({ query, variables });

  return new Promise<T>((resolve, reject) => {
    const url = new URL("https://api.linear.app/graphql");
    const req = request(
      {
        hostname: url.hostname,
        path: url.pathname,
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: LINEAR_API_KEY,
          "Content-Length": Buffer.byteLength(body),
        },
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on("data", (chunk: Buffer) => chunks.push(chunk));
        res.on("end", () => {
          try {
            const text = Buffer.concat(chunks).toString("utf-8");
            const json = JSON.parse(text) as {
              data?: T;
              errors?: Array<{ message: string }>;
            };
            if (json.errors?.length) {
              reject(new Error(`Linear API error: ${json.errors[0].message}`));
              return;
            }
            resolve(json.data as T);
          } catch (err) {
            reject(err);
          }
        });
      },
    );

    req.setTimeout(30_000, () => {
      req.destroy();
      reject(new Error("Linear API request timed out"));
    });

    req.on("error", (err) => reject(err));
    req.write(body);
    req.end();
  });
}

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

describe.skipIf(!canRun)("tracker-linear (integration)", () => {
  const tracker = trackerLinear.create();

  const project: ProjectConfig = {
    name: "test-project",
    repo: "test-org/test-repo",
    path: "/tmp/test",
    defaultBranch: "main",
    sessionPrefix: "test",
    tracker: {
      plugin: "linear",
      teamId: LINEAR_TEAM_ID!,
    },
  };

  // Issue state tracked across tests (created in beforeAll, cleaned up in afterAll)
  let issueIdentifier: string; // e.g. "INT-1234"
  let issueUuid: string | undefined; // Linear internal UUID (needed for trash via direct API)

  // -------------------------------------------------------------------------
  // Setup — create a test issue
  // -------------------------------------------------------------------------

  beforeAll(async () => {
    const result = await tracker.createIssue!(
      {
        title: `[AO Integration Test] ${new Date().toISOString()}`,
        description: "Automated integration test issue. Safe to delete if found lingering.",
        priority: 4, // Low
      },
      project,
    );

    issueIdentifier = result.id;

    // Resolve the UUID for cleanup — only possible with direct API key
    if (LINEAR_API_KEY) {
      const data = await linearGraphQL<{ issue: { id: string } }>(
        `query($id: String!) { issue(id: $id) { id } }`,
        { id: issueIdentifier },
      );
      issueUuid = data.issue.id;
    }
  }, 30_000);

  // -------------------------------------------------------------------------
  // Cleanup — archive the test issue so it doesn't clutter the board.
  // With LINEAR_API_KEY we can trash it directly. With Composio-only we
  // close it via the plugin (can't trash through the plugin interface).
  // -------------------------------------------------------------------------

  afterAll(async () => {
    if (!issueIdentifier) return;

    try {
      if (issueUuid && LINEAR_API_KEY) {
        await linearGraphQL(
          `mutation($id: String!) {
            issueUpdate(id: $id, input: { trashed: true }) {
              success
            }
          }`,
          { id: issueUuid },
        );
      } else {
        // Composio-only: best-effort close via plugin
        await tracker.updateIssue!(issueIdentifier, { state: "closed" }, project);
      }
    } catch {
      // Best-effort cleanup
    }
  }, 15_000);

  // -------------------------------------------------------------------------
  // Test cases
  // -------------------------------------------------------------------------

  it("createIssue returns a well-shaped Issue", () => {
    // Validating the result captured in beforeAll
    expect(issueIdentifier).toBeDefined();
    expect(issueIdentifier).toMatch(/^[A-Z]+-\d+$/);
  });

  it("getIssue fetches the created issue with correct fields", async () => {
    const issue = await tracker.getIssue(issueIdentifier, project);

    expect(issue.id).toBe(issueIdentifier);
    expect(issue.title).toContain("[AO Integration Test]");
    expect(issue.description).toContain("Automated integration test");
    expect(issue.url).toMatch(/^https:\/\/linear\.app\//);
    expect(issue.state).toBe("open");
    expect(Array.isArray(issue.labels)).toBe(true);
    expect(issue.priority).toBe(4);
  });

  it("isCompleted returns false for an open issue", async () => {
    const completed = await tracker.isCompleted(issueIdentifier, project);
    expect(completed).toBe(false);
  });

  it("issueUrl returns a valid Linear URL", () => {
    const url = tracker.issueUrl(issueIdentifier, project);
    expect(url).toContain(issueIdentifier);
    expect(url).toMatch(/^https:\/\/linear\.app\//);
  });

  it("branchName returns conventional branch name", () => {
    const branch = tracker.branchName(issueIdentifier, project);
    expect(branch).toBe(`feat/${issueIdentifier}`);
  });

  it("generatePrompt includes issue details", async () => {
    const prompt = await tracker.generatePrompt(issueIdentifier, project);

    expect(prompt).toContain(issueIdentifier);
    expect(prompt).toContain("[AO Integration Test]");
    expect(prompt).toContain("Priority: Low");
    expect(prompt).toContain("implement the changes");
  });

  it("listIssues includes the created issue", async () => {
    const issues = await tracker.listIssues!({ state: "open", limit: 50 }, project);

    const found = issues.find((i: { id: string }) => i.id === issueIdentifier);
    expect(found).toBeDefined();
    expect(found!.title).toContain("[AO Integration Test]");
  });

  it("updateIssue adds a comment", async () => {
    await tracker.updateIssue!(issueIdentifier, { comment: "Integration test comment" }, project);

    // Verify the comment was added — use direct API if available,
    // otherwise trust the plugin didn't throw
    if (LINEAR_API_KEY) {
      const data = await linearGraphQL<{
        issue: { comments: { nodes: Array<{ body: string }> } };
      }>(
        `query($id: String!) {
          issue(id: $id) {
            comments { nodes { body } }
          }
        }`,
        { id: issueIdentifier },
      );

      const commentBodies = data.issue.comments.nodes.map((c) => c.body);
      expect(commentBodies).toContain("Integration test comment");
    }
  });

  it("updateIssue closes the issue and isCompleted reflects it", async () => {
    await tracker.updateIssue!(issueIdentifier, { state: "closed" }, project);

    // Linear API has eventual consistency — poll until the state propagates
    const completed = await pollUntilEqual(
      () => tracker.isCompleted(issueIdentifier, project),
      true,
      { timeoutMs: 5_000, intervalMs: 500 },
    );
    expect(completed).toBe(true);

    const issue = await tracker.getIssue(issueIdentifier, project);
    expect(issue.state).toBe("closed");
  });

  it("updateIssue reopens the issue", async () => {
    await tracker.updateIssue!(issueIdentifier, { state: "open" }, project);

    // Linear API has eventual consistency — poll until the state propagates
    const completed = await pollUntilEqual(
      () => tracker.isCompleted(issueIdentifier, project),
      false,
      { timeoutMs: 5_000, intervalMs: 500 },
    );
    expect(completed).toBe(false);

    const issue = await tracker.getIssue(issueIdentifier, project);
    expect(issue.state).toBe("open");
  });
});
