import { describe, it, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import axios from 'axios';
import { getPRDiff, filterDiff, parseReviewIgnore, getReviewIgnore, DEFAULT_REVIEW_IGNORE, postComment, postPullReviewComment, postPullReview, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit, getBotReviewOutcome, listPullReviews, getPullReviewComments, listAllReviewComments, resolvePullReviewComment, getFileContentAtRef } from '../gitea.js';

afterEach(() => mock.restoreAll());

describe('gitea', () => {
  it('getPRDiff calls Gitea diff API with Authorization header', async () => {
    let capturedUrl, capturedOpts;
    mock.method(axios, 'get', async (url, opts) => {
      capturedUrl = url;
      capturedOpts = opts;
      return { data: 'diff content' };
    });
    const result = await getPRDiff();
    assert.equal(result, 'diff content');
    assert.ok(capturedUrl.includes('/api/v1/repos/'));
    assert.ok(capturedUrl.endsWith('.diff'));
    assert.ok(capturedOpts.headers['Authorization'].startsWith('token '));
    assert.equal(capturedOpts.headers['Content-Type'], 'application/json');
  });

  it('postComment calls Gitea issues comments API with body', async () => {
    let capturedUrl, capturedBody, capturedOpts;
    mock.method(axios, 'post', async (url, body, opts) => {
      capturedUrl = url;
      capturedBody = body;
      capturedOpts = opts;
      return { data: { id: 1 } };
    });
    const result = await postComment('hello world');
    assert.deepEqual(result, { id: 1 });
    assert.ok(capturedUrl.includes('/api/v1/repos/'));
    assert.ok(capturedUrl.endsWith('/comments'));
    assert.equal(capturedBody.body, 'hello world');
    assert.ok(capturedOpts.headers['Authorization'].startsWith('token '));
  });

  it('sets an insecure httpsAgent by default', async () => {
    let capturedOpts;
    mock.method(axios, 'get', async (_url, opts) => {
      capturedOpts = opts;
      return { data: '' };
    });
    await getPRDiff();
    assert.equal(capturedOpts.httpsAgent.options.rejectUnauthorized, false);
  });

  it('getPRDiff propagates axios errors', async () => {
    mock.method(axios, 'get', async () => { throw new Error('network error'); });
    await assert.rejects(() => getPRDiff(), /network error/);
  });

  it('postComment propagates axios errors', async () => {
    mock.method(axios, 'post', async () => { throw new Error('api error'); });
    await assert.rejects(() => postComment('test'), /api error/);
  });

  it('postPullReviewComment posts an inline review comment to the pulls reviews API', async () => {
    let capturedUrl, capturedBody, capturedOpts;
    mock.method(axios, 'post', async (url, body, opts) => {
      capturedUrl = url;
      capturedBody = body;
      capturedOpts = opts;
      return { data: { id: 7 } };
    });
    const result = await postPullReviewComment({ path: 'app/preflight.js', line: 19, body: 'inline body' });
    assert.deepEqual(result, { id: 7 });
    assert.ok(capturedUrl.includes('/api/v1/repos/'));
    assert.ok(capturedUrl.endsWith('/reviews'));
    assert.equal(capturedBody.event, 'COMMENT');
    assert.equal(capturedBody.comments.length, 1);
    assert.equal(capturedBody.comments[0].path, 'app/preflight.js');
    assert.equal(capturedBody.comments[0].new_position, 19);
    assert.equal(capturedBody.comments[0].body, 'inline body');
    assert.ok(capturedOpts.headers['Authorization'].startsWith('token '));
  });

  it('postPullReviewComment propagates axios errors', async () => {
    mock.method(axios, 'post', async () => { throw new Error('not in diff'); });
    await assert.rejects(() => postPullReviewComment({ path: 'a.js', line: 1, body: 'x' }), /not in diff/);
  });

  it('postPullReview posts one review with multiple comments', async () => {
    let capturedUrl, capturedBody, capturedOpts;
    mock.method(axios, 'post', async (url, body, opts) => {
      capturedUrl = url;
      capturedBody = body;
      capturedOpts = opts;
      return { data: { id: 9 } };
    });

    const result = await postPullReview({
      body: 'summary',
      comments: [{ path: 'app/a.js', new_position: 10, body: 'comment' }],
    });

    assert.deepEqual(result, { id: 9 });
    assert.ok(capturedUrl.includes('/api/v1/repos/'));
    assert.ok(capturedUrl.endsWith('/reviews'));
    assert.equal(capturedBody.event, 'COMMENT');
    assert.equal(capturedBody.body, 'summary');
    assert.equal(capturedBody.comments.length, 1);
    assert.equal(capturedBody.comments[0].path, 'app/a.js');
    assert.equal(capturedBody.comments[0].new_position, 10);
    assert.equal(capturedBody.comments[0].body, 'comment');
    assert.ok(capturedOpts.headers['Authorization'].startsWith('token '));
  });

  it('getCommitMessageBySha reads commit message from Gitea API', async () => {
    let capturedUrl;
    mock.method(axios, 'get', async (url) => {
      capturedUrl = url;
      return { data: { message: 'chore: update ai-review findings [ai-review-bot]' } };
    });
    const message = await getCommitMessageBySha('abc123');
    assert.ok(capturedUrl.includes('/git/commits/abc123'));
    assert.ok(message.includes('[ai-review-bot]'));
  });

  it('getBranchHeadCommitMessage reads branch head commit message from Gitea API', async () => {
    const urls = [];
    mock.method(axios, 'get', async (url) => {
      urls.push(url);
      if (url.includes('/branches/feat%2Ftest')) {
        return { data: { commit: { id: 'abc123' } } };
      }
      return { data: { message: 'chore: update ai-review findings [ai-review-bot]' } };
    });
    const message = await getBranchHeadCommitMessage('feat/test');
    assert.ok(urls.some(url => url.includes('/branches/feat%2Ftest')));
    assert.ok(urls.some(url => url.includes('/git/commits/abc123')));
    assert.ok(message.includes('[ai-review-bot]'));
  });

  it('listPullReviews returns review array from the pulls reviews API', async () => {
    let capturedUrl;
    mock.method(axios, 'get', async (url) => {
      capturedUrl = url;
      return { data: [{ id: 1 }, { id: 2 }] };
    });
    const reviews = await listPullReviews();
    assert.equal(reviews.length, 2);
    assert.ok(capturedUrl.endsWith('/reviews'));
  });

  it('getPullReviewComments fetches comments of a specific review', async () => {
    let capturedUrl;
    mock.method(axios, 'get', async (url) => {
      capturedUrl = url;
      return { data: [{ id: 11, body: 'x' }] };
    });
    const comments = await getPullReviewComments(7);
    assert.equal(comments.length, 1);
    assert.ok(capturedUrl.includes('/reviews/7/comments'));
  });

  it('listAllReviewComments flattens comments across reviews and skips failing ones', async () => {
    mock.method(axios, 'get', async (url) => {
      if (url.endsWith('/reviews')) return { data: [{ id: 1 }, { id: 2 }] };
      if (url.includes('/reviews/1/comments')) return { data: [{ id: 11 }, { id: 12 }] };
      throw new Error('boom');
    });
    const comments = await listAllReviewComments();
    assert.equal(comments.length, 2);
    assert.deepEqual(comments.map(c => c.id), [11, 12]);
  });

  it('resolvePullReviewComment posts to the official resolve endpoint', async () => {
    let capturedUrl, capturedOpts;
    mock.method(axios, 'post', async (url, _body, opts) => {
      capturedUrl = url;
      capturedOpts = opts;
      return { data: { ok: true } };
    });
    await resolvePullReviewComment(42);
    assert.ok(capturedUrl.endsWith('/pulls/comments/42/resolve'));
    assert.ok(capturedOpts.headers['Authorization'].startsWith('token '));
  });

  it('getFileContentAtRef decodes base64 file content and passes ref param', async () => {
    let capturedUrl, capturedOpts;
    mock.method(axios, 'get', async (url, opts) => {
      capturedUrl = url;
      capturedOpts = opts;
      return { data: { content: Buffer.from('hello\nworld', 'utf8').toString('base64'), encoding: 'base64' } };
    });
    const content = await getFileContentAtRef('app/x.js', 'abc123');
    assert.equal(content, 'hello\nworld');
    assert.ok(capturedUrl.includes('/contents/app/x.js'));
    assert.equal(capturedOpts.params.ref, 'abc123');
  });

  it('getFileContentAtRef returns empty string on error', async () => {
    mock.method(axios, 'get', async () => { throw new Error('404'); });
    assert.equal(await getFileContentAtRef('missing.js', 'ref'), '');
  });

  it('shouldSkipBotCommit returns true when either sha or branch head is a bot success commit, but not failure', async () => {
    mock.method(axios, 'get', async (url) => {
      if (url.includes('/git/commits/sha-bot')) {
        return { data: { message: 'chore: update ai-review findings [ai-review-bot][failure]' } };
      }
      if (url.includes('/git/commits/sha-success')) {
        return { data: { message: 'chore: update ai-review findings [ai-review-bot][success]' } };
      }
      if (url.includes('/branches/feat%2Ftest')) {
        return { data: { commit: { id: 'sha-bot' } } };
      }
      if (url.includes('/branches/feat%2Fsuccess')) {
        return { data: { commit: { id: 'sha-success' } } };
      }
      return { data: { message: 'regular commit' } };
    });
    await assert.equal(await shouldSkipBotCommit({ sha: 'sha-bot', branch: 'feat/test' }), false);
    await assert.equal(await shouldSkipBotCommit({ sha: 'sha-success', branch: 'feat/success' }), true);
    await assert.equal(await shouldSkipBotCommit({ sha: 'sha-success', branch: 'feat/test' }), true);
    assert.equal(getBotReviewOutcome('chore: update ai-review findings [ai-review-bot][failure]'), 'failure');
    assert.equal(getBotReviewOutcome('chore: update ai-review findings [ai-review-bot][success]'), 'success');
    assert.equal(getBotReviewOutcome('chore: update ai-review findings [ai-review-bot]'), 'unknown');
  });
});

describe('filterDiff', () => {
  const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`;

  it('filters out configured folder blocks', () => {
    const diff = block('.gitea/workflows/review.yaml') + block('.github/workflows/review.yaml') + block('src/index.js');
    const result = filterDiff(diff, ['.gitea/', '.github/']);
    assert.ok(!result.includes('.gitea/'));
    assert.ok(!result.includes('.github/'));
    assert.ok(result.includes('src/index.js'));
  });

  it('filters out configured top-level file blocks', () => {
    const diff = block('README.md') + block('src/index.js');
    const result = filterDiff(diff, ['README.md', 'TODO.md']);
    assert.ok(!result.includes('README.md'));
    assert.ok(result.includes('src/index.js'));
  });

  it('returns empty string when all blocks are excluded', () => {
    const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json');
    const result = filterDiff(diff, ['.gitea/']);
    assert.equal(result, '');
  });

  it('returns empty string for empty diff', () => {
    assert.equal(filterDiff('', ['.gitea/']), '');
  });

  it('always drops node_modules blocks at any depth (avoids blowing the LLM input limit)', () => {
    const diff = block('src/node_modules/axios/index.js')
      + block('node_modules/js-yaml/lib.js')
      + block('src/main.js');
    const result = filterDiff(diff, []);
    assert.ok(!result.includes('node_modules'));
    assert.ok(result.includes('src/main.js'));
  });

  it('excludes lock files and dist via the getPRDiff prefix list', () => {
    const diff = block('src/package-lock.json') + block('dist/index.js') + block('src/main.js');
    const result = filterDiff(diff, ['package-lock.json', 'src/package-lock.json', 'dist/']);
    assert.ok(!result.includes('package-lock.json'));
    assert.ok(!result.includes('dist/'));
    assert.ok(result.includes('src/main.js'));
  });
});

describe('parseReviewIgnore', () => {
  it('parses prefixes, skipping blanks and comments', () => {
    const text = '# comment\n\n.gitea/\n  dist/  \n# another\nREADME.md\n';
    assert.deepEqual(parseReviewIgnore(text), ['.gitea/', 'dist/', 'README.md']);
  });

  it('returns an empty array for empty/nullish input', () => {
    assert.deepEqual(parseReviewIgnore(''), []);
    assert.deepEqual(parseReviewIgnore(null), []);
  });
});

describe('getReviewIgnore', () => {
  it('uses patterns fetched from .reviewignore when present', async () => {
    mock.method(axios, 'get', async () => ({
      data: { content: Buffer.from('a/\nb/\n# c\n').toString('base64'), encoding: 'base64' },
    }));
    const patterns = await getReviewIgnore();
    assert.deepEqual(patterns, ['a/', 'b/']);
  });

  it('falls back to the default list when .reviewignore is missing/empty', async () => {
    mock.method(axios, 'get', async () => ({ data: {} }));
    const patterns = await getReviewIgnore();
    assert.deepEqual(patterns, DEFAULT_REVIEW_IGNORE);
  });
});
