import { describe, test, expect } from "bun:test";
import type { Logger } from "pino";
import type { RepoConfig, JiraIssue } from "@/types/index.js";
import { REPO_SELECTOR } from "@/core/constants.js";
import { RepoSelector } from "@/repo/selector.js";
import type { RepoRegistry } from "@/repo/registry.js";

// ── Helpers ──────────────────────────────────────────────────────────────────

function makeRepo(overrides: Partial<RepoConfig> & { name: string }): RepoConfig {
  return {
    url: `git@github.com:org/${overrides.name}.git`,
    description: "",
    keywords: [],
    ...overrides,
  };
}

function makeIssue(overrides: Partial<JiraIssue> = {}): JiraIssue {
  return {
    id: "10001",
    key: "TEST-1",
    summary: "",
    description: null,
    issueType: "Bug",
    status: "Open",
    created: null,
    updated: null,
    acceptanceCriteria: null,
    stacktrace: null,
    flow: null,
    assignee: null,
    parent: null,
    subtasks: [],
    children: [],
    attachments: [],
    comments: [],
    components: [],
    ...overrides,
  };
}

function stubRegistry(repos: RepoConfig[]): RepoRegistry {
  return { getAll: () => repos } as unknown as RepoRegistry;
}

const stubLogger: Logger = {
  warn: () => {},
  info: () => {},
} as unknown as Logger;

// ── Keyword match scoring ────────────────────────────────────────────────────

describe("RepoSelector — keyword scoring", () => {
  test("keyword match scores KEYWORD_WEIGHT (2) per keyword", () => {
    const repo = makeRepo({ name: "api", keywords: ["backend"], description: "API service" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "backend error in production" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("api");
  });

  test("keyword match scores higher than description word match", () => {
    // repo-a: keyword "deploy" matches (score = KEYWORD_WEIGHT = 2)
    // repo-b: description word "deploy" matches (score = DESCRIPTION_WORD_WEIGHT = 1)
    const repoA = makeRepo({ name: "infra", keywords: ["deploy"], description: "Infrastructure" });
    const repoB = makeRepo({
      name: "docs",
      keywords: ["documentation"],
      description: "deploy guides and docs",
    });
    const registry = stubRegistry([repoA, repoB]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "deploy failed" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("infra");
  });

  test("multiple keyword matches accumulate score", () => {
    const repo = makeRepo({ name: "api", keywords: ["backend", "server"], description: "" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    // Both keywords present in text -> score = 2 * KEYWORD_WEIGHT = 4
    const result = selector.select(makeIssue({ summary: "backend server crash" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("api");
  });
});

// ── Description word scoring ─────────────────────────────────────────────────

describe("RepoSelector — description word scoring", () => {
  test("description words longer than 2 chars contribute DESCRIPTION_WORD_WEIGHT (1)", () => {
    // "service" (7 chars) from description should match ticket word "service"
    const repo = makeRepo({ name: "svc", keywords: [], description: "core service layer" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "service timeout" }));
    // score = 1 (one description word match), which equals MIN_SCORE_THRESHOLD
    expect(result).not.toBeNull();
    expect(result!.name).toBe("svc");
  });

  test("description words of 2 chars or fewer are excluded from scoring", () => {
    // "UI" normalizes to "ui" which is 2 chars -> filtered out by .filter(w => w.length > 2)
    const repo = makeRepo({ name: "web", keywords: [], description: "UI" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "ui is broken" }));
    // No keywords, no description words pass the >2 filter -> score = 0 -> null
    expect(result).toBeNull();
  });
});

// ── Sub-threshold / no match ─────────────────────────────────────────────────

describe("RepoSelector — sub-threshold", () => {
  test("returns null when no keywords and no description overlap", () => {
    const repo = makeRepo({
      name: "api",
      keywords: ["backend"],
      description: "REST service",
    });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(
      makeIssue({ summary: "completely unrelated words that do not match anything" }),
    );
    expect(result).toBeNull();
  });

  test("returns null when ticket text is empty", () => {
    const repo = makeRepo({ name: "api", keywords: ["backend"], description: "API" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "" }));
    expect(result).toBeNull();
  });
});

// ── Tie-breaking ─────────────────────────────────────────────────────────────

describe("RepoSelector — tie-breaking", () => {
  test("equal-scored repos return the one that appears first in registry order", () => {
    const repoA = makeRepo({ name: "alpha", keywords: ["deploy"], description: "" });
    const repoB = makeRepo({ name: "bravo", keywords: ["deploy"], description: "" });
    const registry = stubRegistry([repoA, repoB]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "deploy issue" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("alpha");
  });
});

// ── Case-insensitive keyword matching ────────────────────────────────────────

describe("RepoSelector — case insensitivity", () => {
  test("keyword matching is case-insensitive", () => {
    const repo = makeRepo({ name: "api", keywords: ["Backend"], description: "" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "BACKEND error" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("api");
  });
});

// ── Stacktrace included in corpus ────────────────────────────────────────────

describe("RepoSelector — stacktrace in corpus", () => {
  test("stacktrace text contributes to keyword matching", () => {
    const repo = makeRepo({ name: "api", keywords: ["nullpointer"], description: "" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(
      makeIssue({
        summary: "crash on login",
        stacktrace: "java.lang.NullPointer at com.example.Main",
      }),
    );
    expect(result).not.toBeNull();
    expect(result!.name).toBe("api");
  });
});

// ── Parent context included ──────────────────────────────────────────────────

describe("RepoSelector — parent context in corpus", () => {
  test("parent summary and description contribute to matching", () => {
    const repo = makeRepo({ name: "mobile", keywords: ["android"], description: "" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(
      makeIssue({
        summary: "fix crash",
        parent: {
          id: "10000",
          key: "PARENT-1",
          summary: "Android app stability",
          description: null,
          issueType: "Story",
          status: "In Progress",
          attachments: [],
        },
      }),
    );
    expect(result).not.toBeNull();
    expect(result!.name).toBe("mobile");
  });
});

// ── Multi-keyword phrase matching ────────────────────────────────────────────

describe("RepoSelector — multi-word keyword", () => {
  test("multi-word keyword matches via includes on normalized text", () => {
    const repo = makeRepo({ name: "mobile", keywords: ["react native"], description: "" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "React Native crash on iOS" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("mobile");
  });
});

// ── Keyword normalization (punctuation) ──────────────────────────────────────

describe("RepoSelector — keyword normalization", () => {
  test("keyword with punctuation matches normalized ticket text", () => {
    // ticket text "Next.js" normalizes to "next js"; keyword "next.js" must
    // normalize the same way for the match to land.
    const repo = makeRepo({ name: "web", keywords: ["next.js"], description: "" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "Upgrade Next.js to 15" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("web");
  });

  test("keyword that normalizes to empty string does not match every ticket", () => {
    const repo = makeRepo({ name: "web", keywords: ["!!!"], description: "" });
    const registry = stubRegistry([repo]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "anything at all" }));
    expect(result).toBeNull();
  });
});

// ── Multi-repo ordering ─────────────────────────────────────────────────────

describe("RepoSelector — multi-repo selection", () => {
  test("returns the repo with the highest score among multiple candidates", () => {
    // repoA: 2 keyword matches = 4 points
    // repoB: 1 keyword match = 2 points
    const repoA = makeRepo({ name: "api", keywords: ["backend", "server"], description: "" });
    const repoB = makeRepo({ name: "web", keywords: ["server"], description: "" });
    const registry = stubRegistry([repoA, repoB]);
    const selector = new RepoSelector(registry, stubLogger);

    const result = selector.select(makeIssue({ summary: "backend server timeout" }));
    expect(result).not.toBeNull();
    expect(result!.name).toBe("api");
  });
});

// ── Scoring weight constants ─────────────────────────────────────────────────

describe("RepoSelector — scoring constants", () => {
  test("KEYWORD_WEIGHT is 2 and DESCRIPTION_WORD_WEIGHT is 1", () => {
    expect(REPO_SELECTOR.KEYWORD_WEIGHT).toBe(2);
    expect(REPO_SELECTOR.DESCRIPTION_WORD_WEIGHT).toBe(1);
    expect(REPO_SELECTOR.MIN_SCORE_THRESHOLD).toBe(1);
  });
});
