import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
  tallyResponses,
  resultsVisible,
  winningAnswerIds,
  parsePollStart,
  parseResponseAnswerIds,
  validateSelections,
  PollResponse,
} from './poll';

const me = '@me:server';

test('tallyResponses counts one vote per selection', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1'] },
    { sender: '@b:s', ts: 1, answerIds: ['1'] },
    { sender: '@c:s', ts: 1, answerIds: ['2'] },
  ];
  const { counts, total } = tallyResponses(responses, me);
  assert.equal(counts.get('1'), 2);
  assert.equal(counts.get('2'), 1);
  assert.equal(total, 3);
});

test('tallyResponses keeps only the latest response per sender', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1'] },
    { sender: '@a:s', ts: 5, answerIds: ['2'] }, // changed vote
    { sender: '@a:s', ts: 3, answerIds: ['1'] }, // older, ignored
  ];
  const { counts, total } = tallyResponses(responses, me);
  assert.equal(counts.get('1'), undefined);
  assert.equal(counts.get('2'), 1);
  assert.equal(total, 1);
});

test('tallyResponses handles multi-select and reports myVotes', () => {
  const responses: PollResponse[] = [
    { sender: me, ts: 2, answerIds: ['1', '3'] },
    { sender: '@b:s', ts: 1, answerIds: ['3'] },
  ];
  const { counts, myVotes, total } = tallyResponses(responses, me);
  assert.equal(counts.get('1'), 1);
  assert.equal(counts.get('3'), 2);
  assert.deepEqual([...myVotes].sort(), ['1', '3']);
  assert.equal(total, 2);
});

test('tallyResponses: a cleared (empty) latest response removes the voter', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1'] },
    { sender: '@a:s', ts: 2, answerIds: [] }, // latest is empty → sender dropped entirely
  ];
  const { counts, total } = tallyResponses(responses, me);
  assert.equal(counts.get('1'), undefined);
  assert.equal(total, 0);
});

test('tallyResponses: re-voting after an empty clear counts the newest non-empty', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1'] },
    { sender: '@a:s', ts: 2, answerIds: [] }, // cleared
    { sender: '@a:s', ts: 3, answerIds: ['2'] }, // re-voted, newest wins
  ];
  const { counts, total } = tallyResponses(responses, me);
  assert.equal(counts.get('1'), undefined);
  assert.equal(counts.get('2'), 1);
  assert.equal(total, 1);
});

test('tallyResponses: voters list the senders per answer, consistent with counts', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1'] },
    { sender: '@b:s', ts: 1, answerIds: ['1'] },
    { sender: '@c:s', ts: 1, answerIds: ['2'] },
  ];
  const { counts, voters } = tallyResponses(responses, me);
  assert.deepEqual([...(voters.get('1') ?? [])].sort(), ['@a:s', '@b:s']);
  assert.deepEqual(voters.get('2'), ['@c:s']);
  // voters must always agree with counts
  for (const [id, list] of voters) {
    assert.equal(list.length, counts.get(id));
  }
});

test('tallyResponses: a re-vote moves the sender to the new answer in voters', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1'] },
    { sender: '@a:s', ts: 5, answerIds: ['2'] }, // changed vote
  ];
  const { voters } = tallyResponses(responses, me);
  assert.equal(voters.get('1'), undefined); // no longer under the old answer
  assert.deepEqual(voters.get('2'), ['@a:s']);
});

test('tallyResponses: a cleared latest response drops the sender from every voter list', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1', '2'] },
    { sender: '@a:s', ts: 2, answerIds: [] }, // cleared
  ];
  const { voters } = tallyResponses(responses, me);
  assert.equal(voters.get('1'), undefined);
  assert.equal(voters.get('2'), undefined);
});

test('tallyResponses: multi-select lists a sender under each chosen answer', () => {
  const responses: PollResponse[] = [
    { sender: '@a:s', ts: 1, answerIds: ['1', '3'] },
    { sender: '@b:s', ts: 1, answerIds: ['3'] },
  ];
  const { voters } = tallyResponses(responses, me);
  assert.deepEqual(voters.get('1'), ['@a:s']);
  assert.deepEqual([...(voters.get('3') ?? [])].sort(), ['@a:s', '@b:s']);
});

test('resultsVisible: disclosed always, undisclosed only when ended', () => {
  assert.equal(resultsVisible(false, false), true); // disclosed, open
  assert.equal(resultsVisible(false, true), true); // disclosed, ended
  assert.equal(resultsVisible(true, false), false); // undisclosed, open → hidden
  assert.equal(resultsVisible(true, true), true); // undisclosed, ended → revealed
});

test('winningAnswerIds returns the single top answer', () => {
  const counts = new Map([
    ['1', 3],
    ['2', 1],
  ]);
  assert.deepEqual(winningAnswerIds(counts), ['1']);
});

test('winningAnswerIds returns all tied answers', () => {
  const counts = new Map([
    ['1', 2],
    ['2', 2],
    ['3', 1],
  ]);
  assert.deepEqual(winningAnswerIds(counts).sort(), ['1', '2']);
});

test('winningAnswerIds is empty when there are no votes', () => {
  assert.deepEqual(winningAnswerIds(new Map()), []);
});

// --- wire-format round-trip: exactly what PollCreator + handleVote send (STABLE) ---

test('parsePollStart reads the STABLE m.poll format Lotus sends', () => {
  const content = {
    'm.poll': {
      question: { 'm.text': 'Favorite color?' },
      answers: [
        { 'm.id': '0', 'm.text': 'Red' },
        { 'm.id': '1', 'm.text': 'Blue' },
      ],
      max_selections: 1,
      kind: 'm.poll.disclosed',
    },
    body: 'Favorite color?\n1. Red\n2. Blue',
    msgtype: 'm.text',
  };
  const parsed = parsePollStart(content);
  assert.ok(parsed);
  assert.equal(parsed!.question, 'Favorite color?');
  assert.deepEqual(parsed!.answers, [
    { id: '0', text: 'Red' },
    { id: '1', text: 'Blue' },
  ]);
  assert.equal(parsed!.maxSelections, 1);
  assert.equal(parsed!.isUndisclosed, false);
});

test('parsePollStart reads an UNSTABLE (Element-authored) poll + defaults kind to undisclosed', () => {
  const content = {
    'org.matrix.msc3381.poll.start': {
      question: { 'org.matrix.msc1767.text': 'Lunch?' },
      answers: [
        { id: 'a', 'org.matrix.msc1767.text': 'Pizza' },
        { id: 'b', 'org.matrix.msc1767.text': 'Tacos' },
      ],
      max_selections: 2,
      // no kind → undisclosed per spec
    },
  };
  const parsed = parsePollStart(content);
  assert.ok(parsed);
  assert.equal(parsed!.question, 'Lunch?');
  assert.deepEqual(parsed!.answers, [
    { id: 'a', text: 'Pizza' },
    { id: 'b', text: 'Tacos' },
  ]);
  assert.equal(parsed!.maxSelections, 2);
  assert.equal(parsed!.isUndisclosed, true);
});

test('parsePollStart returns null for non-poll content', () => {
  assert.equal(parsePollStart({ body: 'hi', msgtype: 'm.text' }), null);
});

test('parseResponseAnswerIds reads stable m.selections and unstable nested answers', () => {
  assert.deepEqual(parseResponseAnswerIds({ 'm.selections': ['0', '1'] }), ['0', '1']);
  assert.deepEqual(
    parseResponseAnswerIds({ 'org.matrix.msc3381.poll.response': { answers: ['a'] } }),
    ['a'],
  );
  assert.deepEqual(parseResponseAnswerIds({}), []);
});

test('validateSelections filters unknown ids, de-dupes, and caps to max_selections', () => {
  const valid = new Set(['0', '1', '2']);
  assert.deepEqual(validateSelections(['0', 'x', '1'], valid, 3), ['0', '1']); // drops unknown 'x'
  assert.deepEqual(validateSelections(['1', '1', '2'], valid, 3), ['1', '2']); // de-dupes
  assert.deepEqual(validateSelections(['0', '1', '2'], valid, 1), ['0']); // caps to first max_selections
  assert.deepEqual(validateSelections(['nope'], valid, 3), []); // all invalid → spoiled/empty
});
