"""Tests for test_runner.validate.results_writer."""

from __future__ import annotations

import json
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from test_runner.validate.results_writer import (
    _ddl_statements,
    write_results_batch,
    write_results_local,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _result(
    procedure: str = "dbo.GetProducts",
    params_hash: str = "abc123",
    status: str = "PASS",
    match_type: str = "data_match",
    baseline_rows: int = 10,
    actual_rows: int = 10,
    error: str = "",
    executed_at: str = "2026-01-01T00:00:00Z",
    **extra,
) -> dict:
    r = dict(
        procedure=procedure,
        params_hash=params_hash,
        status=status,
        match_type=match_type,
        baseline_rows=baseline_rows,
        actual_rows=actual_rows,
        error=error,
        executed_at=executed_at,
    )
    r.update(extra)
    return r


# ---------------------------------------------------------------------------
# TestWriteResultsLocal
# ---------------------------------------------------------------------------

class TestWriteResultsLocal:
    def test_creates_output_dir_if_missing(self, tmp_path: Path):
        target = tmp_path / "new" / "nested" / "dir"
        write_results_local([_result()], target)
        assert target.is_dir()

    def test_writes_json_file(self, tmp_path: Path):
        write_results_local([_result(status="PASS"), _result(status="FAIL")], tmp_path)
        json_file = tmp_path / "results.json"
        assert json_file.exists()
        data = json.loads(json_file.read_text())
        assert len(data) == 2
        assert data[0]["status"] == "PASS"

    def test_json_includes_in_memory_diff(self, tmp_path: Path):
        diff = {"match": False, "row_counts": {"baseline": 5, "actual": 3}}
        r = _result(status="FAIL", in_memory_diff=diff)
        write_results_local([r], tmp_path)
        data = json.loads((tmp_path / "results.json").read_text())
        assert data[0]["in_memory_diff"]["row_counts"]["baseline"] == 5

    def test_empty_results_writes_empty_json_array(self, tmp_path: Path):
        write_results_local([], tmp_path)
        data = json.loads((tmp_path / "results.json").read_text())
        assert data == []

    def test_idempotent_overwrites_previous_output(self, tmp_path: Path):
        write_results_local([_result(status="PASS")], tmp_path)
        write_results_local([_result(status="FAIL")], tmp_path)
        data = json.loads((tmp_path / "results.json").read_text())
        assert data[0]["status"] == "FAIL"

    def test_non_serialisable_values_handled(self, tmp_path: Path):
        from datetime import datetime
        r = _result(executed_at=datetime(2026, 1, 1))
        write_results_local([r], tmp_path)
        data = json.loads((tmp_path / "results.json").read_text())
        assert "2026" in data[0]["executed_at"]

    def test_row_dumps_written_when_present(self, tmp_path: Path):
        baseline = [{"Col": "a"}, {"Col": "b"}]
        actual   = [{"Col": "a"}, {"Col": "x"}]
        r = _result(params_hash="deadbeef",
                    **{"_baseline_rows": baseline, "_actual_rows": actual})
        write_results_local([r], tmp_path)
        b_path = tmp_path / "deadbeef_baseline.json"
        a_path = tmp_path / "deadbeef_actual.json"
        assert b_path.exists()
        assert a_path.exists()
        assert json.loads(b_path.read_text()) == baseline
        assert json.loads(a_path.read_text()) == actual

    def test_private_keys_stripped_from_results_json(self, tmp_path: Path):
        r = _result(params_hash="abc",
                    **{"_baseline_rows": [{"X": 1}], "_actual_rows": [{"X": 2}]})
        write_results_local([r], tmp_path)
        data = json.loads((tmp_path / "results.json").read_text())
        assert "_baseline_rows" not in data[0]
        assert "_actual_rows" not in data[0]

    def test_no_row_dumps_when_absent(self, tmp_path: Path):
        write_results_local([_result(params_hash="nohash")], tmp_path)
        assert not list(tmp_path.glob("*_baseline.json"))
        assert not list(tmp_path.glob("*_actual.json"))


# ---------------------------------------------------------------------------
# TestDdlStatements
# ---------------------------------------------------------------------------

class TestDdlStatements:
    def test_results_table_is_id_and_data_only(self):
        stmts = _ddl_statements("MYDB")
        table_ddl = next(s for s in stmts if "CREATE TABLE" in s and "RESULTS" in s and "BASELINE" not in s)
        assert "data VARIANT" in table_ddl
        assert "procedure_name" not in table_ddl


    def test_results_detail_view_exists(self):
        ddl = "\n".join(_ddl_statements("MYDB"))
        assert "RESULTS_DETAIL" in ddl

    def test_results_detail_view_projects_all_fields(self):
        stmts = _ddl_statements("MYDB")
        detail_ddl = next(s for s in stmts if "RESULTS_DETAIL" in s)
        for field in ("procedure_name", "params_hash", "status", "executed_at",
                      "modifies_data", "affected_tables", "source_checksum", "converted_checksum"):
            assert field in detail_ddl, f"expected '{field}' in RESULTS_DETAIL DDL"

    @pytest.mark.parametrize("view", ["SUMMARY", "LATEST", "FAILURES"])
    def test_view_references_results_detail(self, view):
        stmts = _ddl_statements("MYDB")
        view_ddl = next(s for s in stmts if f"VIEW MYDB.VALIDATION.{view} " in s)
        assert "RESULTS_DETAIL" in view_ddl


# ---------------------------------------------------------------------------
# TestWriteResultsBatch
# ---------------------------------------------------------------------------

class TestWriteResultsBatch:
    def _make_connector(self) -> MagicMock:
        connector = MagicMock()
        connector.execute_statement = MagicMock()
        return connector

    def _result_with_metadata(self) -> dict:
        return _result(
            procedure="dbo.MyProc",
            metadata={
                "modifies_data": True,
                "affected_tables": ["DB.dbo.Orders"],
                "source_checksum": "abc123",
                "converted_checksum": "def456",
            },
            executed_at="2026-01-01T00:00:00+00:00",
        )

    def test_inserts_single_variant_column(self):
        connector = self._make_connector()
        write_results_batch(connector, [self._result_with_metadata()], "MYDB")
        sql = connector.execute_statement.call_args[0][0]
        assert "INSERT INTO MYDB.VALIDATION.RESULTS (data)" in sql
        assert "PARSE_JSON(" in sql

    @pytest.mark.parametrize("key", ["procedure", "params_hash", "status", "metadata", "modifies_data"])
    def test_result_field_present_in_data_json(self, key):
        connector = self._make_connector()
        write_results_batch(connector, [self._result_with_metadata()], "MYDB")
        sql = connector.execute_statement.call_args[0][0]
        assert key in sql

    @pytest.mark.parametrize("key", ["_baseline_rows", "_actual_rows", "in_memory_diff"])
    def test_private_key_excluded_from_data(self, key):
        connector = self._make_connector()
        r = _result(**{"_baseline_rows": [{"x": 1}], "_actual_rows": [{"x": 2}],
                       "in_memory_diff": {"match": False}})
        write_results_batch(connector, [r], "MYDB")
        sql = connector.execute_statement.call_args[0][0]
        assert key not in sql

    def test_dollar_quoting_used_for_parse_json(self):
        connector = self._make_connector()
        write_results_batch(connector, [self._result_with_metadata()], "MYDB")
        sql = connector.execute_statement.call_args[0][0]
        assert "PARSE_JSON($$" in sql
        assert "$$)" in sql

    def test_embedded_json_with_backslash_quotes(self):
        """Params containing embedded JSON (e.g. \\\"key\\\") must survive
        PARSE_JSON via dollar-sign quoting."""
        connector = self._make_connector()
        r = _result(
            params={"Colors": '[{"ColorName":"Burgundy"}]'},
        )
        write_results_batch(connector, [r], "MYDB")
        sql = connector.execute_statement.call_args[0][0]
        assert "PARSE_JSON($$" in sql
        assert '\\"ColorName\\"' in sql or '"ColorName"' in sql

    def test_empty_batch_executes_nothing(self):
        connector = self._make_connector()
        write_results_batch(connector, [], "MYDB")
        connector.execute_statement.assert_not_called()
