import type { Octokit } from '@octokit/rest';
import { describe, expect, it, vi } from 'vitest';

import {
  compareAcrossForkNetwork,
  resolveBranchHead,
} from '../../convex/githubClient';

describe('resolveBranchHead', () => {
  it('returns the branch tip sha from the branch ref', async () => {
    const getBranch = vi.fn(async () => ({
      data: { commit: { sha: 'deadbeef' } },
    }));
    const octokit = {
      rest: { repos: { getBranch } },
    } as unknown as Octokit;

    const result = await resolveBranchHead(octokit, 'o', 'r', 'main');

    expect(result.sha).toBe('deadbeef');
    expect(getBranch).toHaveBeenCalledWith({
      owner: 'o',
      repo: 'r',
      branch: 'main',
    });
  });
});

const makeCommit = (sha: string) => ({
  sha,
  commit: {
    message: `commit ${sha}`,
    author: { name: 'Dev', email: 'dev@example.com', date: '2026-01-01T00:00:00Z' },
    committer: { name: 'Dev', email: 'dev@example.com', date: '2026-01-01T00:00:00Z' },
  },
  author: { login: 'dev' },
  html_url: `https://github.com/o/r/commit/${sha}`,
});

describe('compareAcrossForkNetwork', () => {
  it('paginates the compare and takes headSha from the branch tip, not the last commit', async () => {
    const page1 = Array.from({ length: 100 }, (_, i) => makeCommit(`c${i}`));
    // Last commit in the collected list is a DIFFERENT sha than the branch tip.
    const page2 = Array.from({ length: 50 }, (_, i) =>
      makeCommit(i === 49 ? 'LAST_COMMIT_NOT_TIP' : `c${100 + i}`),
    );

    const compareCommitsWithBasehead = vi.fn(
      async ({ page }: { page?: number }) => {
        const commits = page === 2 ? page2 : page1;
        return {
          data: {
            total_commits: 150,
            ahead_by: 150,
            merge_base_commit: { sha: 'MERGE_BASE' },
            base_commit: { sha: 'BASE' },
            html_url: 'https://github.com/o/r/compare/base...head',
            commits,
          },
        };
      },
    );
    const getBranch = vi.fn(async () => ({
      data: { commit: { sha: 'TIP' } },
    }));
    const octokit = {
      rest: { repos: { compareCommitsWithBasehead, getBranch } },
    } as unknown as Octokit;

    const result = await compareAcrossForkNetwork(octokit, {
      owner: 'up',
      repo: 'upstream-repo',
      baseOwner: 'fork',
      baseBranch: 'main',
      headOwner: 'up',
      headRepo: 'upstream-repo',
      headBranch: 'main',
    });

    // Core bug: head is the branch tip, NOT the last commit in the compare list.
    expect(result.headSha).toBe('TIP');
    expect(result.commits[result.commits.length - 1]?.sha).toBe(
      'LAST_COMMIT_NOT_TIP',
    );
    expect(result.headSha).not.toBe('LAST_COMMIT_NOT_TIP');

    // ahead_by / merge base / base come from the first compare page.
    expect(result.aheadBy).toBe(150);
    expect(result.mergeBaseSha).toBe('MERGE_BASE');
    expect(result.baseSha).toBe('BASE');

    // All commits collected across both pages.
    expect(result.commits).toHaveLength(150);

    // Pagination is bounded and the tip is resolved via a single branch ref call.
    expect(getBranch).toHaveBeenCalledTimes(1);
    expect(getBranch).toHaveBeenCalledWith({
      owner: 'up',
      repo: 'upstream-repo',
      branch: 'main',
    });
    expect(compareCommitsWithBasehead).toHaveBeenCalledTimes(2);
    expect(compareCommitsWithBasehead.mock.calls[0]?.[0]).toMatchObject({
      per_page: 100,
      page: 1,
    });
  });
});
