import { expect, test } from 'bun:test'; import { chmodSync,mkdirSync,mkdtempSync,truncateSync,writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { attachmentNeedsTextMessage,attachmentPayload,parseArgs,shouldCoalesce,telegramMultipart,validateAttachments } from './send.js'; import { resolveChannel } from './resolve.js';

const tmp=()=>{const base=join(process.env.XDG_CACHE_HOME??join(process.env.HOME??'/tmp','.cache'),'overdeck/tests');mkdirSync(base,{recursive:true});return mkdtempSync(join(base,'botmaster-send-'))};

// 'fyi' doubles as the default priority, so an exclusivity check written against the
// current value rejected the plain --fyi call and took the whole notify path down.
test('--fyi is accepted on its own', () => {
  expect(parseArgs(['--fyi', 'hello']).priority).toBe('fyi');
});

test('--needs-answer is accepted on its own', () => {
  expect(parseArgs(['--needs-answer', 'hello']).priority).toBe('needs-answer');
});

test('no priority flag defaults to fyi', () => {
  expect(parseArgs(['hello']).priority).toBe('fyi');
});

test('--group and --text behave as --channel and positional text', () => {
  expect(parseArgs(['--group', 'overdeck', '--text', 'hello world'])).toEqual(parseArgs(['--channel', 'overdeck', 'hello world']));
  expect(parseArgs(['--group=no-such-channel', '--text=hello'])).toMatchObject({ channel: 'no-such-channel', text: 'hello' });
  expect(() => resolveChannel([], parseArgs(['--group', 'no-such-channel', '--text', 'hello']).channel)).toThrow(/unknown channel/);
});

test.each([
  ['--fyi', '--needs-answer'],
  ['--needs-answer', '--fyi'],
  ['--fyi', '--fyi'],
])('%s with %s is refused', (a, b) => {
  expect(() => parseArgs([a, b, 'hello'])).toThrow(/mutually exclusive/);
});

test('attachments parse repeatedly and require text',()=>{expect(parseArgs(['--attachment','one.txt','--attachment=two.txt','hello']).attachments).toEqual(['one.txt','two.txt']);expect(()=>parseArgs(['--attachment','one.txt'])).toThrow(/requires non-empty text/);expect(()=>parseArgs(['--inbox','--attachment','one.txt'])).toThrow(/--inbox takes no message/)});

test('attachment validation refuses missing, non-regular, unreadable, empty, oversize, secret-shaped, and home mode 0600 files',()=>{const root=tmp(),repo=join(root,'repo'),home=join(root,'home'),file=(name:string,body='x')=>{const path=join(root,name);mkdirSync(join(path,'..'),{recursive:true});writeFileSync(path,body);return path};mkdirSync(repo);mkdirSync(home);const missing=join(root,'missing'),directory=root,unreadable=file('unreadable');chmodSync(unreadable,0);const empty=file('empty',''),oversize=file('oversize');truncateSync(oversize,45*1024*1024+1);const secret=file('token.pem'),secretDirectory=file('.secrets/dump.txt'),privateFile=join(home,'private');writeFileSync(privateFile,'x');chmodSync(privateFile,0o600);for(const [path,pattern] of [[missing,/does not exist/],[directory,/regular file/],[unreadable,/unreadable/],[empty,/empty/],[oversize,/45 MB/],[secret,/secret-shaped/],[secretDirectory,/secret-shaped/],[privateFile,/0600/]] as const)expect(()=>validateAttachments([path],home,repo)).toThrow(pattern);chmodSync(unreadable,0o644)});

test('multipart body includes document and fields',async()=>{const root=tmp(),path=join(root,'receipt.txt');writeFileSync(path,'receipt');let seen:FormData|undefined;const original=globalThis.fetch;globalThis.fetch=(async(_url:any,init:any)=>{seen=init.body;return new Response(JSON.stringify({ok:true,result:{message_id:7}}),{status:200})}) as typeof fetch;try{expect(await telegramMultipart('token','sendDocument',{chat_id:-1,caption:'context'},{path,realpath:path,bytes:7})).toBe(7);expect(await seen!.get('chat_id')).toBe('-1');expect(await seen!.get('caption')).toBe('context');expect((seen!.get('document') as File).name).toBe('receipt.txt')}finally{globalThis.fetch=original}});

test('caption overflow sends text separately and leaves document bare',()=>{const text='x'.repeat(1025);expect(attachmentNeedsTextMessage(text)).toBe(true);expect(attachmentPayload(text,1,0,-1,'fyi')).not.toHaveProperty('caption');expect(attachmentPayload('short',1,0,-1,'fyi')).toMatchObject({caption:'short'});expect(attachmentPayload('short',2,42,-1,'fyi')).toMatchObject({reply_to_message_id:42})});

test('attachments skip fyi coalescing',()=>{expect(shouldCoalesce(parseArgs(['hello']))).toBe(true);expect(shouldCoalesce(parseArgs(['--attachment','receipt.txt','hello']))).toBe(false)});

test('--raw sends verbatim, skips coalescing, and refuses reply or attachments',()=>{expect(parseArgs(['--raw','Message #m123 received'])).toMatchObject({raw:true,text:'Message #m123 received'});expect(shouldCoalesce(parseArgs(['--raw','Message #m123 received']))).toBe(false);expect(()=>parseArgs(['--raw','--reply','m123','hello'])).toThrow(/cannot be combined/);expect(()=>parseArgs(['--raw','--attachment','receipt.txt','hello'])).toThrow(/cannot be combined/)});
