"""Unit tests for Odoo 12 CLI — synthetic data, no external dependencies."""

import json
import os
import sys
import subprocess
import pytest

from cli_anything.odoo_12.core.record import parse_domain, parse_values, parse_fields
from cli_anything.odoo_12.core.session import Session, HistoryEntry
from cli_anything.odoo_12.utils.odoo_backend import (
    get_connection_params, load_config, save_config, OdooRPC, OdooBackend,
)
from cli_anything.odoo_12.utils.odoo_rest import OdooREST
from cli_anything.odoo_12.core import aliases
from cli_anything.odoo_12.core import nlq as nlq_mod
from cli_anything.odoo_12.core.formatters import _format_brl, _field_label, _format_cell
from cli_anything.odoo_12.core.cross_group import resolve_cross_group, CROSS_GROUP_REGISTRY


# ── Fixtures ───────────────────────────────────────────────────

@pytest.fixture
def tmp_dir(tmp_path):
    return str(tmp_path)


@pytest.fixture
def session_file(tmp_path):
    return str(tmp_path / "test_session.json")


@pytest.fixture
def config_file(tmp_path, monkeypatch):
    cfg_path = tmp_path / "config.json"
    monkeypatch.setattr(
        "cli_anything.odoo_12.utils.odoo_backend_base.CONFIG_FILE", cfg_path
    )
    monkeypatch.setattr(
        "cli_anything.odoo_12.utils.odoo_backend_base.CONFIG_DIR", tmp_path
    )
    monkeypatch.setattr(
        "cli_anything.odoo_12.core.connection.load_config",
        lambda: load_config.__wrapped__() if hasattr(load_config, "__wrapped__") else _load_cfg(cfg_path),
    )
    return cfg_path


def _load_cfg(path):
    if path.exists():
        with open(path) as f:
            return json.load(f)
    return {}


# ── parse_domain tests ─────────────────────────────────────────

class TestParseDomain:
    def test_empty_string(self):
        assert parse_domain("") == []

    def test_empty_brackets(self):
        assert parse_domain("[]") == []

    def test_json_format(self):
        result = parse_domain('[["is_company","=",true]]')
        assert result == [["is_company", "=", True]]

    def test_python_format(self):
        result = parse_domain("[('is_company','=',True)]")
        assert result == [("is_company", "=", True)]

    def test_complex_domain(self):
        result = parse_domain("[('name','ilike','test'),('active','=',True)]")
        assert len(result) == 2

    def test_invalid_format(self):
        with pytest.raises(ValueError, match="Invalid domain"):
            parse_domain("not a domain")


# ── parse_values tests ─────────────────────────────────────────

class TestParseValues:
    def test_empty(self):
        assert parse_values("") == {}

    def test_json_format(self):
        result = parse_values('{"name": "Test", "active": true}')
        assert result == {"name": "Test", "active": True}

    def test_python_format(self):
        result = parse_values("{'name': 'Test', 'active': True}")
        assert result == {"name": "Test", "active": True}

    def test_invalid_format(self):
        with pytest.raises(ValueError, match="Invalid values"):
            parse_values("not a dict")


# ── parse_fields tests ─────────────────────────────────────────

class TestParseFields:
    def test_empty(self):
        assert parse_fields("") is None

    def test_single_field(self):
        assert parse_fields("name") == ["name"]

    def test_multiple_fields(self):
        assert parse_fields("name,email,phone") == ["name", "email", "phone"]

    def test_whitespace(self):
        assert parse_fields("name, email , phone") == ["name", "email", "phone"]

    def test_trailing_comma(self):
        result = parse_fields("name,email,")
        assert result == ["name", "email"]


# ── Session tests ──────────────────────────────────────────────

class TestSession:
    def test_record(self, session_file):
        sess = Session(session_file=session_file)
        sess.record("test cmd", {"key": "val"}, {"result": "ok"})
        assert sess.history_count == 1

    def test_undo(self, session_file):
        sess = Session(session_file=session_file)
        sess.record("cmd1", {})
        sess.record("cmd2", {})
        entry = sess.undo()
        assert entry is not None
        assert entry.command == "cmd2"
        assert sess.history_count == 1
        assert sess.can_redo

    def test_redo(self, session_file):
        sess = Session(session_file=session_file)
        sess.record("cmd1", {})
        sess.undo()
        entry = sess.redo()
        assert entry is not None
        assert entry.command == "cmd1"
        assert sess.history_count == 1
        assert not sess.can_redo

    def test_undo_empty(self, session_file):
        sess = Session(session_file=session_file)
        assert sess.undo() is None

    def test_redo_empty(self, session_file):
        sess = Session(session_file=session_file)
        assert sess.redo() is None

    def test_history_limit(self, session_file):
        sess = Session(session_file=session_file)
        for i in range(10):
            sess.record(f"cmd{i}", {})
        hist = sess.history(limit=3)
        assert len(hist) == 3
        assert hist[0]["command"] == "cmd7"

    def test_status(self, session_file):
        sess = Session(session_file=session_file)
        sess.record("cmd1", {})
        sess.record("cmd2", {})
        sess.undo()
        status = sess.status()
        assert status["history_count"] == 1
        assert status["can_undo"] is True
        assert status["can_redo"] is True
        assert status["redo_count"] == 1

    def test_persistence(self, session_file):
        sess1 = Session(session_file=session_file)
        sess1.record("cmd1", {"a": 1})
        sess1.record("cmd2", {"b": 2})

        sess2 = Session(session_file=session_file)
        assert sess2.history_count == 2
        hist = sess2.history()
        assert hist[0]["command"] == "cmd1"
        assert hist[1]["command"] == "cmd2"

    def test_record_clears_redo(self, session_file):
        sess = Session(session_file=session_file)
        sess.record("cmd1", {})
        sess.undo()
        assert sess.can_redo
        sess.record("cmd2", {})
        assert not sess.can_redo


# ── HistoryEntry tests ─────────────────────────────────────────

class TestHistoryEntry:
    def test_to_dict(self):
        entry = HistoryEntry(command="test", args={"key": "val"})
        d = entry.to_dict()
        assert d["command"] == "test"
        assert d["args"] == {"key": "val"}
        assert "timestamp" in d

    def test_from_dict(self):
        entry = HistoryEntry.from_dict({
            "command": "test",
            "args": {"key": "val"},
            "timestamp": "2024-01-01T00:00:00",
        })
        assert entry.command == "test"
        assert entry.timestamp == "2024-01-01T00:00:00"


# ── Connection config tests ────────────────────────────────────

class TestConnectionConfig:
    def test_get_connection_params_defaults(self, monkeypatch):
        monkeypatch.setattr(
            "cli_anything.odoo_12.utils.odoo_backend_base.load_config", lambda: {}
        )
        params = get_connection_params()
        assert params["url"] == "http://localhost:8069"
        assert params["db"] == "test"
        assert params["user"] == "admin"

    def test_get_connection_params_override(self, monkeypatch):
        monkeypatch.setattr(
            "cli_anything.odoo_12.utils.odoo_backend_base.load_config", lambda: {}
        )
        params = get_connection_params(url="http://remote:8069", db="prod")
        assert params["url"] == "http://remote:8069"
        assert params["db"] == "prod"

    def test_get_connection_params_from_config(self, monkeypatch):
        monkeypatch.setattr(
            "cli_anything.odoo_12.utils.odoo_backend_base.load_config",
            lambda: {"url": "http://saved:8069", "db": "saved_db",
                     "user": "user1", "password": "pass1"},
        )
        params = get_connection_params()
        assert params["url"] == "http://saved:8069"
        assert params["db"] == "saved_db"


# ── Config file I/O tests ─────────────────────────────────────

class TestConfigIO:
    def test_save_and_load(self, tmp_path, monkeypatch):
        cfg_file = tmp_path / "config.json"
        monkeypatch.setattr("cli_anything.odoo_12.utils.odoo_backend_base.CONFIG_FILE", cfg_file)
        monkeypatch.setattr("cli_anything.odoo_12.utils.odoo_backend_base.CONFIG_DIR", tmp_path)

        save_config({"url": "http://test:8069", "db": "mydb"})
        loaded = load_config()
        assert loaded["url"] == "http://test:8069"
        assert loaded["db"] == "mydb"

    def test_load_missing_file(self, tmp_path, monkeypatch):
        monkeypatch.setattr(
            "cli_anything.odoo_12.utils.odoo_backend_base.CONFIG_FILE",
            tmp_path / "nonexistent.json"
        )
        result = load_config()
        assert result == {}


# ── OdooRPC constructor tests ─────────────────────────────────

class TestOdooRPCInit:
    def test_constructor(self):
        rpc = OdooRPC("http://localhost:8069", "test", "admin", "admin")
        assert rpc.url == "http://localhost:8069"
        assert rpc.db == "test"
        assert rpc.user == "admin"
        assert rpc.uid is None

    def test_strips_trailing_slash(self):
        rpc = OdooRPC("http://localhost:8069/", "test", "admin", "admin")
        assert rpc.url == "http://localhost:8069"

    def test_is_odoo_backend(self):
        rpc = OdooRPC("http://localhost:8069", "test", "admin", "admin")
        assert isinstance(rpc, OdooBackend)


class TestOdooRESTInit:
    def test_constructor(self):
        rpc = OdooREST("http://localhost:8069", "test", "admin", "admin",
                       client_id="cid", client_secret="csecret")
        assert rpc.url == "http://localhost:8069"
        assert rpc.client_id == "cid"
        assert rpc.uid is None

    def test_strips_trailing_slash(self):
        rpc = OdooREST("http://localhost:8069/", "test", "admin", "admin")
        assert rpc.url == "http://localhost:8069"

    def test_is_odoo_backend(self):
        rpc = OdooREST("http://localhost:8069", "test", "admin", "admin")
        assert isinstance(rpc, OdooBackend)


# ── CLI subprocess tests ──────────────────────────────────────

def _resolve_cli(name):
    """Resolve installed CLI command; falls back to python -m for dev."""
    import shutil
    force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1"
    path = shutil.which(name)
    if path:
        print(f"[_resolve_cli] Using installed command: {path}")
        return [path]
    if force:
        raise RuntimeError(f"{name} not found in PATH. Install with: pip install -e .")
    module = name.replace("cli-anything-", "cli_anything.") + "." + name.split("-")[-1] + "_cli"
    print(f"[_resolve_cli] Falling back to: {sys.executable} -m {module}")
    return [sys.executable, "-m", module]


class TestCLISubprocess:
    CLI_BASE = _resolve_cli("cli-anything-odoo_12")

    def _run(self, args, check=True):
        return subprocess.run(
            self.CLI_BASE + args,
            capture_output=True, text=True,
            check=check,
        )

    def test_help(self):
        result = self._run(["--help"])
        assert result.returncode == 0
        assert "Odoo 12 CLI" in result.stdout

    def test_help_record(self):
        result = self._run(["record", "--help"])
        assert result.returncode == 0
        assert "search" in result.stdout

    def test_help_module(self):
        result = self._run(["module", "--help"])
        assert result.returncode == 0
        assert "list" in result.stdout

    def test_help_db(self):
        result = self._run(["db", "--help"])
        assert result.returncode == 0
        assert "list" in result.stdout

    def test_help_export(self):
        result = self._run(["export", "--help"])
        assert result.returncode == 0
        assert "csv" in result.stdout

    def test_help_report(self):
        result = self._run(["report", "--help"])
        assert result.returncode == 0
        assert "render" in result.stdout

    def test_help_session(self):
        result = self._run(["session", "--help"])
        assert result.returncode == 0
        assert "history" in result.stdout

    def test_help_ask(self):
        result = self._run(["ask", "--help"])
        assert result.returncode == 0
        assert "Natural language" in result.stdout


# ── Aliases tests ─────────────────────────────────────────────

class TestAliases:
    def test_resolve_ptbr(self):
        result = aliases.resolve_model("liste todas as faturas")
        assert result is not None
        model, fields, alias, implicit = result
        assert model == "account.invoice"
        assert alias == "faturas"

    def test_resolve_english(self):
        result = aliases.resolve_model("show me all invoices")
        assert result is not None
        assert result[0] == "account.invoice"

    def test_multi_word_priority(self):
        """Multi-word aliases like 'pedido de venda' should match before 'venda'."""
        result = aliases.resolve_model("pedido de venda do cliente")
        assert result is not None
        assert result[0] == "sale.order"

    def test_unknown_model(self):
        result = aliases.resolve_model("something completely unknown")
        assert result is None

    def test_implicit_domain_clientes(self):
        result = aliases.resolve_model("liste os clientes")
        assert result is not None
        model, fields, alias, implicit = result
        assert model == "res.partner"
        assert ("customer", "=", True) in implicit

    def test_status_clause(self):
        assert aliases.get_status_clause("faturas pagas") == ("state", "=", "paid")
        assert aliases.get_status_clause("open invoices") == ("state", "=", "open")

    def test_type_clause(self):
        assert aliases.get_type_clause("faturas de entrada") == ("type", "=", "in_invoice")

    def test_boolean_clauses(self):
        clauses = aliases.get_boolean_clauses("parceiros ativos")
        assert ("active", "=", True) in clauses


# ── NLQ Parser tests ──────────────────────────────────────────

class TestNLQParser:
    def test_simple_model(self):
        parsed = nlq_mod.parse("liste todas as empresas")
        assert parsed.model == "res.company"
        assert isinstance(parsed.domain, list)

    def test_status_filter(self):
        parsed = nlq_mod.parse("faturas pagas")
        assert parsed.model == "account.invoice"
        assert ("state", "=", "paid") in parsed.domain

    def test_month_year(self):
        parsed = nlq_mod.parse("faturas em janeiro/2024")
        assert parsed.model == "account.invoice"
        assert ("date_invoice", ">=", "2024-01-01") in parsed.domain
        assert ("date_invoice", "<=", "2024-01-31") in parsed.domain

    def test_month_only(self):
        """Month without year defaults to current year."""
        from datetime import datetime
        parsed = nlq_mod.parse("faturas em março")
        year = datetime.now().year
        assert ("date_invoice", ">=", f"{year}-03-01") in parsed.domain

    def test_date_range(self):
        parsed = nlq_mod.parse("faturas de 01/03 a 15/03")
        assert parsed.model == "account.invoice"
        # Should have date range domain
        date_domains = [d for d in parsed.domain if isinstance(d, tuple) and d[0] == "date_invoice"]
        assert len(date_domains) >= 2

    def test_relative_this_month(self):
        from datetime import datetime
        parsed = nlq_mod.parse("faturas este mês")
        now = datetime.now()
        expected_start = f"{now.year}-{now.month:02d}-01"
        assert ("date_invoice", ">=", expected_start) in parsed.domain

    def test_relative_last_month(self):
        parsed = nlq_mod.parse("faturas último mês")
        # Should have date domain for previous month
        date_domains = [d for d in parsed.domain if isinstance(d, tuple) and d[0] == "date_invoice"]
        assert len(date_domains) == 2

    def test_birthday(self):
        parsed = nlq_mod.parse("aniversariantes de março")
        assert parsed.model == "res.partner"
        assert ("birthday", "!=", False) in parsed.domain

    def test_type_invoice(self):
        parsed = nlq_mod.parse("faturas de entrada")
        assert ("type", "=", "in_invoice") in parsed.domain

    def test_boolean_active(self):
        parsed = nlq_mod.parse("clientes ativos")
        assert ("active", "=", True) in parsed.domain
        assert ("customer", "=", True) in parsed.domain

    def test_cpf_cnpj(self):
        parsed = nlq_mod.parse("parceiro com CPF 123.456.789-00")
        # Should have vat/name search
        assert "|" in parsed.domain

    def test_amount_above_br(self):
        parsed = nlq_mod.parse("faturas acima de R$ 10.000,00")
        assert ("amount_total", ">=", 10000.0) in parsed.domain

    def test_amount_above_en(self):
        parsed = nlq_mod.parse("invoices above 5000")
        assert ("amount_total", ">=", 5000.0) in parsed.domain

    def test_export_csv(self):
        parsed = nlq_mod.parse("faturas e exporte para csv em /tmp/fat.csv")
        assert parsed.export is not None
        assert parsed.export.format == "csv"
        assert parsed.export.path == "/tmp/fat.csv"

    def test_export_json(self):
        parsed = nlq_mod.parse("empresas e exporte para json em /tmp/emp.json")
        assert parsed.export is not None
        assert parsed.export.format == "json"
        assert parsed.export.path == "/tmp/emp.json"

    def test_complex_combined(self):
        parsed = nlq_mod.parse(
            "faturas de entrada pagas em janeiro/2024 acima de R$ 5.000 e exporte para csv em /tmp/f.csv"
        )
        assert parsed.model == "account.invoice"
        assert ("type", "=", "in_invoice") in parsed.domain
        assert ("state", "=", "paid") in parsed.domain
        assert ("date_invoice", ">=", "2024-01-01") in parsed.domain
        assert ("amount_total", ">=", 5000.0) in parsed.domain
        assert parsed.export is not None
        assert parsed.export.format == "csv"

    def test_unknown_model_error(self):
        with pytest.raises(ValueError, match="Modelo não reconhecido"):
            nlq_mod.parse("xyz foobar baz")

    def test_limit(self):
        parsed = nlq_mod.parse("top 10 empresas")
        assert parsed.limit == 10

    def test_title_composition(self):
        parsed = nlq_mod.parse("faturas de entrada pagas")
        assert "Entrada" in parsed.title
        assert parsed.title != ""


# ── Formatters tests ──────────────────────────────────────────

class TestFormatters:
    def test_format_brl(self):
        assert _format_brl(1234.56) == "1.234,56"
        assert _format_brl(0) == "0,00"
        assert _format_brl(None) == "—"
        assert _format_brl(False) == "—"

    def test_field_label(self):
        assert _field_label("partner_id") == "Parceiro"
        assert _field_label("amount_total") == "Total (R$)"
        assert _field_label("unknown_field") == "Unknown Field"

    def test_format_cell(self):
        assert _format_cell(None) == "—"
        assert _format_cell(False) == "—"
        assert _format_cell((1, "Partner Name")) == "Partner Name"
        assert _format_cell(True) == "Sim"
        assert _format_cell("hello") == "hello"


# ── Cross-group tests ────────────────────────────────────────

class TestCrossGroup:
    def test_resolve_pos_product_categ(self):
        key = resolve_cross_group("pos.order", "categoria de produto")
        assert key == "product_categ"

    def test_resolve_pos_pos_categ(self):
        key = resolve_cross_group("pos.order", "categoria")
        assert key == "pos_categ"

    def test_resolve_pos_product(self):
        key = resolve_cross_group("pos.order", "produto")
        assert key == "product"

    def test_resolve_invoice_product_categ(self):
        key = resolve_cross_group("account.invoice", "categoria de produto")
        assert key == "product_categ"

    def test_resolve_invoice_categ_fallback(self):
        """For non-POS models, 'categoria' alone defaults to product_categ."""
        key = resolve_cross_group("account.invoice", "categoria")
        assert key == "product_categ"

    def test_resolve_sale_order(self):
        key = resolve_cross_group("sale.order", "product category")
        assert key == "product_categ"

    def test_resolve_unknown(self):
        key = resolve_cross_group("res.partner", "categoria")
        assert key is None

    def test_registry_has_all_models(self):
        models = set(k[0] for k in CROSS_GROUP_REGISTRY.keys())
        assert "pos.order" in models
        assert "account.invoice" in models
        assert "sale.order" in models
        assert "purchase.order" in models

    def test_nlq_cross_group_detection(self):
        parsed = nlq_mod.parse("vendas do pos de janeiro/2026 agrupado por categoria de produto")
        assert parsed.cross_group_key == "product_categ"
        assert parsed.group_by is None

    def test_nlq_simple_group_when_no_cross(self):
        parsed = nlq_mod.parse("vendas do pos de janeiro/2026 agrupado por ponto de venda")
        assert parsed.cross_group_key is None
        assert parsed.group_by == "config_id"

    def test_nlq_cross_group_by_product(self):
        parsed = nlq_mod.parse("vendas do pos agrupado por produto")
        assert parsed.cross_group_key == "product"
