# (c) cavaliba.com - tests / cli "cavaliba load" --page/--size and --first/--last

import json
import os
import tempfile
from io import StringIO

import yaml
from django.core.management import call_command
from django.test import TestCase

import app_home.cache as cache
from app_data.data import Instance
from tests.helper import add_schema


class CliLoadPageSizeTest(TestCase):
    """--page/--size (new, all formats) and --first/--last (legacy, previously
    CSV-only) both slice the parsed object list before writing. Locks in the
    --page/--size <-> --first/--last translation, common to all three formats:
    load_file_csv() slices while iterating (cursor-based, no full parse needed);
    load_file_yaml()/load_file_json() take the same first/last params but can only
    slice after the whole file is parsed (YAML/JSON have no per-row streaming)."""

    def setUp(self):
        cache.clear()
        add_schema(
            classname="cliloadpagesize",
            field_definition={"mystring": {"dataformat": "string", "displayname": "MyString"}},
        )

    def _write(self, suffix, content):
        fd, path = tempfile.mkstemp(suffix=suffix)
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(content)
        return path

    def _objects(self, n):
        return [
            {"classname": "cliloadpagesize", "keyname": f"row{i}", "mystring": f"hello{i}"}
            for i in range(1, n + 1)
        ]

    def _csv_file(self, n):
        content = (
            "keyname,schema,mystring\n"
            + "\n".join(f"row{i},cliloadpagesize,hello{i}" for i in range(1, n + 1))
            + "\n"
        )
        return self._write(".csv", content)

    def _yaml_file(self, n):
        return self._write(".yaml", yaml.safe_dump(self._objects(n)))

    def _json_file(self, n):
        return self._write(".json", json.dumps(self._objects(n)))

    def _exists(self, keyname):
        return Instance.from_keyname(classname="cliloadpagesize", keyname=keyname, expand=False)

    def test_plain_json_load_creates_instances(self):
        """Regression test: cavaliba_load() used to call load_file_json(filename, ...)
        positionally, which bound to that function's first param ("file", expecting
        a file object) instead of "filename" (a path string) - so json.load(file)
        received a plain string and crashed with "'str' object has no attribute
        'read'" on every single `cavaliba load *.json` call, page/size or not."""

        path = self._json_file(3)
        try:
            output = StringIO()
            call_command("cavaliba", "load", path, verbosity=2, stdout=output)
        finally:
            os.remove(path)

        self.assertIn("load done", output.getvalue())
        for i in (1, 2, 3):
            self.assertIsNotNone(self._exists(f"row{i}"))

    def test_page_size_slices_csv(self):
        path = self._csv_file(10)
        try:
            output = StringIO()
            call_command(
                "cavaliba",
                "load",
                path,
                "--page",
                "2",
                "--size",
                "3",
                verbosity=2,
                stdout=output,
            )
        finally:
            os.remove(path)

        for i in (1, 2, 3, 7, 8, 9, 10):
            self.assertIsNone(self._exists(f"row{i}"), f"row{i} should not have been loaded")
        for i in (4, 5, 6):
            self.assertIsNotNone(self._exists(f"row{i}"), f"row{i} should have been loaded")

    def test_page_size_slices_yaml(self):
        """--page/--size translates to first/last, passed straight into
        load_file_yaml(), which slices after parsing the whole file."""

        path = self._yaml_file(10)
        try:
            output = StringIO()
            call_command(
                "cavaliba",
                "load",
                path,
                "--page",
                "2",
                "--size",
                "3",
                verbosity=2,
                stdout=output,
            )
        finally:
            os.remove(path)

        for i in (1, 2, 3, 7, 8, 9, 10):
            self.assertIsNone(self._exists(f"row{i}"))
        for i in (4, 5, 6):
            self.assertIsNotNone(self._exists(f"row{i}"))

    def test_page_size_slices_json(self):
        """Same as YAML above, for load_file_json()."""

        path = self._json_file(10)
        try:
            output = StringIO()
            call_command(
                "cavaliba",
                "load",
                path,
                "--page",
                "2",
                "--size",
                "3",
                verbosity=2,
                stdout=output,
            )
        finally:
            os.remove(path)

        for i in (1, 2, 3, 7, 8, 9, 10):
            self.assertIsNone(self._exists(f"row{i}"))
        for i in (4, 5, 6):
            self.assertIsNotNone(self._exists(f"row{i}"))

    def test_size_alone_takes_first_n_objects(self):
        path = self._json_file(5)
        try:
            output = StringIO()
            call_command("cavaliba", "load", path, "--size", "3", verbosity=2, stdout=output)
        finally:
            os.remove(path)

        for i in (1, 2, 3):
            self.assertIsNotNone(self._exists(f"row{i}"))
        for i in (4, 5):
            self.assertIsNone(self._exists(f"row{i}"))

    def test_page_without_size_is_rejected(self):
        path = self._json_file(3)
        try:
            output = StringIO()
            err = StringIO()
            call_command(
                "cavaliba",
                "load",
                path,
                "--page",
                "2",
                verbosity=2,
                stdout=output,
                stderr=err,
            )
        finally:
            os.remove(path)

        self.assertIn("ERR", err.getvalue())
        for i in (1, 2, 3):
            self.assertIsNone(self._exists(f"row{i}"))

    def test_page_size_and_first_last_together_is_rejected(self):
        path = self._json_file(3)
        try:
            output = StringIO()
            err = StringIO()
            call_command(
                "cavaliba",
                "load",
                path,
                "--page",
                "1",
                "--size",
                "2",
                "--first",
                "1",
                "--last",
                "2",
                verbosity=2,
                stdout=output,
                stderr=err,
            )
        finally:
            os.remove(path)

        self.assertIn("ERR", err.getvalue())
        self.assertIsNone(self._exists("row1"))

    def test_legacy_first_last_still_works_on_yaml(self):
        """Legacy --first/--last, previously a silent no-op for YAML - now
        wired through the same slicing path as --page/--size."""

        path = self._yaml_file(10)
        try:
            output = StringIO()
            call_command(
                "cavaliba",
                "load",
                path,
                "--first",
                "4",
                "--last",
                "6",
                verbosity=2,
                stdout=output,
            )
        finally:
            os.remove(path)

        for i in (1, 2, 3, 7, 8, 9, 10):
            self.assertIsNone(self._exists(f"row{i}"))
        for i in (4, 5, 6):
            self.assertIsNotNone(self._exists(f"row{i}"))
