import json
import tempfile
import time
import unittest
from pathlib import Path
import sys

import site_memory
import text_utils
from trainer import Trainer


class BuildLines(unittest.TestCase):
    def test_accepts_normalizes_and_formats(self):
        lines, rep = site_memory.build_lines([{'question': 'پایتخت ايران كجاست؟', 'answer': 'تهران\nاست.'}])
        self.assertEqual(lines, ['کاربر: پایتخت ایران کجاست | مدل: تهران است.'])
        self.assertEqual((rep['received'], rep['accepted']), (1, 1))

    def test_skips_invalid_and_too_long(self):
        long_answer = 'ا' * 200
        lines, rep = site_memory.build_lines([
            {'question': '', 'answer': 'x'}, {'question': 'سلام', 'answer': '  '},
            {'question': 'بلند', 'answer': long_answer}, {'question': 'سلام', 'answer': 'درود'}])
        self.assertEqual(lines, ['کاربر: سلام | مدل: درود'])
        self.assertEqual((rep['invalid'], rep['tooLong'], rep['accepted']), (2, 1, 1))

    def test_line_fits_block_boundary(self):
        base = 'کاربر: س | مدل: '
        ok = {'question': 'س', 'answer': 'ب' * (site_memory.MAX_LINE - len(base))}
        over = {'question': 'س', 'answer': 'ب' * (site_memory.MAX_LINE - len(base) + 1)}
        self.assertEqual(len(site_memory.build_lines([ok])[0]), 1)
        self.assertEqual(len(site_memory.build_lines([over])[0]), 0)
        self.assertLessEqual(len(site_memory.build_lines([ok])[0][0]) + 1, text_utils.BLOCK)

    def test_huge_entries_are_counted_not_fatal(self):
        # سایت تا ۵۰۰۰ نویسه ذخیره می‌کند؛ این عبارت‌ها فقط باید کنار گذاشته و شمرده شوند
        lines, rep = site_memory.build_lines([
            {'question': 'س' * 5000, 'answer': 'ج'}, {'question': 'سلام', 'answer': 'ج' * 5000},
            {'question': 'درود', 'answer': 'سلام'}])
        self.assertEqual(lines, ['کاربر: درود | مدل: سلام'])
        self.assertEqual((rep['received'], rep['tooLong'], rep['accepted']), (3, 2, 1))

    def test_duplicate_question_last_wins(self):
        lines, rep = site_memory.build_lines([{'question': 'سلام!', 'answer': 'الف'}, {'question': 'سلام', 'answer': 'ب'}])
        self.assertEqual(lines, ['کاربر: سلام | مدل: ب'])
        self.assertEqual(rep['duplicates'], 1)

    def test_pipe_in_question_cannot_break_format(self):
        lines, _ = site_memory.build_lines([{'question': 'الف | مدل: ب', 'answer': 'ج'}])
        self.assertEqual(text_utils.parse_dialogue(lines[0])[1], 'ج')
        self.assertEqual(lines[0].count(' | مدل:'), 1)


class SaveAndLoad(unittest.TestCase):
    def test_save_replaces_and_training_lines_include_it(self):
        with tempfile.TemporaryDirectory() as d:
            f, meta = Path(d) / 'site_memory.txt', Path(d) / 'site_memory.meta.json'
            site_memory.save([{'question': 'الف', 'answer': 'ب'}, {'question': 'پ', 'answer': 'ت'}], f, meta)
            site_memory.save([{'question': 'پ', 'answer': 'ت'}], f, meta)  # عبارت «الف» حذف شد
            text = f.read_text(encoding='utf-8')
            self.assertNotIn('الف', text)
            self.assertIn('کاربر: پ | مدل: ت', text)
            self.assertEqual(site_memory.info(f, meta)['accepted'], 1)
            old = text_utils.SITE_MEMORY_FILE
            try:
                text_utils.SITE_MEMORY_FILE = f
                lines = text_utils.load_training_lines(text_utils.TRAIN_FILE)
            finally:
                text_utils.SITE_MEMORY_FILE = old
            self.assertIn('کاربر: پ | مدل: ت', lines)

    def test_info_when_never_synced(self):
        with tempfile.TemporaryDirectory() as d:
            self.assertEqual(site_memory.info(Path(d) / 'x.txt', Path(d) / 'x.json')['accepted'], 0)


def wait_done(t, timeout=10):
    end = time.time() + timeout
    while t.is_running() and time.time() < end:
        time.sleep(0.05)
    return t.status()


class TrainerTests(unittest.TestCase):
    def test_success_parses_progress(self):
        t = Trainer(cmd=[sys.executable, '-c', "print('step 200/4000 | loss 0.321')"])
        self.assertTrue(t.start())
        st = wait_done(t)
        self.assertEqual((st['state'], st['exitCode'], st['step'], st['totalSteps'], st['loss']),
                         ('succeeded', 0, 200, 4000, 0.321))

    def test_failure_reports_last_line(self):
        t = Trainer(cmd=[sys.executable, '-c', "import sys; print('boom'); sys.exit(3)"])
        t.start()
        st = wait_done(t)
        self.assertEqual((st['state'], st['exitCode'], st['lastLine']), ('failed', 3, 'boom'))

    def test_second_start_rejected_while_running(self):
        t = Trainer(cmd=[sys.executable, '-c', 'import time; time.sleep(1)'])
        self.assertTrue(t.start())
        self.assertFalse(t.start())
        self.assertEqual(wait_done(t)['state'], 'succeeded')

    def test_missing_executable_fails_cleanly(self):
        t = Trainer(cmd=['/nonexistent/binary'])
        self.assertTrue(t.start())
        self.assertEqual(t.status()['state'], 'failed')


if __name__ == '__main__':
    unittest.main()
