"""
PikObs Unified Testing Suite
===========================

This module provides a unified, automated framework for functional testing of 
PikObs tools. Its goal is to ensure that code changes do not introduce 
unexpected regressions in the output.

Core Concept: Reference-Based Testing
---------------------------------------
The system compares the output of a current run against a set of pre-validated
results (the "reference"). If they are identical, the test PASSES. If there are
any differences, it FAILS.

Workflows
---------

**A. Bootstrap Workflow (First-Time Run)**

1.  **Run**: Call the test function pointing to a path that does not exist.
    `pikobs.unittest_timeserie("/path/to/new/reference")`
2.  **Auto-Population**: The script creates the reference with the generated output.
3.  **Manual Verification**: You **must manually review** that the new reference
    files are correct before committing them to version control.

**B. Recommended Development Workflow (Updating an Existing Reference)**

1.  **Modify your code** in PikObs.
2.  **Run the normal test**: `pikobs.unittest_timeserie("/path/to/existing/ref")`.
    **The test is expected to FAIL**. This confirms your change had an effect.
3.  **Verify the new output**: Check the files in the temporary `onthefly_*`
    directory to ensure the new result is what you expected.
4.  **Update the Reference**: If the output is correct, run the test with
    `update_reference=True`. This replaces the old reference with the new one.
    `pikobs.unittest_timeserie("/path/to/existing/ref", update_reference=True)`
5.  **Commit**: Commit both your code changes and the updated reference files.

Extending the Test Suite (Adding New Tests)
-------------------------------------------

The test suite is designed to be an extensible list. To add a test for a new
tool (e.g., `pikobs.profile`), follow these steps:

**Step 1: Create the Test Function in `test_pikobs.py`**

Copy an existing function and adapt it. It must define the specific arguments
for the new tool and call the `run_pikobs_unittest` engine.

.. code-block:: python

   # In pikobs/unittest/test_pikobs.py

   def unittest_profile(reference_path, update_reference=False):
       \"\"\"Run the unified PikObs tests for profile functionality.\"\"\"
       print(f"\\n[*] --- Starting unittest_profile ---")
       ref_path_abs = os.path.abspath(reference_path)
       base_dir = os.path.dirname(ref_path_abs)
       work_path = os.path.join(base_dir, "onthefly_profile")

       # Arguments specific to the 'profile' tool
       func_args = {
           "path_experience_files": "...",
           "experience_name": "...",
           "pathwork": work_path,
           "station_id": "0-20000-0-12345",
           # ...other args...
       }
       
       # Call the test engine with the tool's name
       run_pikobs_unittest("profile", func_args, ref_path_abs, work_path, update_reference)

**Step 2: Expose the New Test Function**

Add the name of your new function to the `pikobs/__init__.py` file to make it
accessible from the main package.

.. code-block:: python

   # In pikobs/__init__.py
   from .unittest.test_pikobs import unittest_timeserie, unittest_scatter, unittest_profile # <-- Add here

List of Available Tests
-----------------------
- `unittest_timeserie`
- `unittest_scatter`
- ... (add new tests here as you create them)

Usage via `python -c`
---------------------
.. code-block:: bash

   # Run a standard regression test
   python -c "import pikobs; pikobs.unittest_timeserie('/path/to/ref')"

   # Update the reference after verifying a change
   python -c "import pikobs; pikobs.unittest_timeserie('/path/to/ref', update_reference=True)"
"""

# pikobs/unittest/test_pikobs.py

"""
PikObs Unified Testing Suite
===========================

This module provides a unified, automated framework for functional testing of 
PikObs data generation and analysis tools (e.g., "timeserie", "scatter", etc.).

Its primary goal is to ensure that changes to the codebase do not introduce
unexpected regressions in the output (files, databases, images).

Core Concept: Reference-Based Testing
--------------------------------------

The methodology is based on comparing the results generated by a current run 
("on-the-fly") against a set of pre-validated, stored results known as the 
"reference" or "golden" dataset.

- If the current output is identical to the reference, the test **PASSES**.
- If there are any differences, the test **FAILS**, alerting the developer.

Workflows
---------

There are two primary workflows for using this test suite: creating the initial
reference, and updating it during development.

**A. Bootstrap Workflow (First-Time Run)**

1.  **Run the Test**: Execute the test function pointing to a reference directory
    that does not yet exist.
    .. code-block:: python
       import pikobs
       pikobs.unittest_timeserie("/path/to/new/reference_dir")
2.  **Auto-Population**: The script runs the PikObs tool and, finding no reference,
    it automatically creates the directory and populates it with the new output.
3.  **Manual Verification**: The test **PASSES** with a warning. You **must** now
    manually inspect every file in the new reference directory to ensure it is
    100% correct. Once verified, commit these files to version control.

**B. Recommended Development Workflow (Updating an Existing Reference)**

This is the standard process when you intentionally change the code in a way
that affects the output.

1.  **Modify Your Code**: Make your changes to `pikobs.timeserie`, fix a bug, or
    add a new feature.

2.  **Run the Test Normally**: Run the test without any special flags.
    .. code-block:: python
       pikobs.unittest_timeserie("/path/to/existing/reference_dir")
    The test **is expected to FAIL**. This is good; it confirms your code
    change had an effect. The new, differing output is saved in the temporary
    `onthefly_*` directory.

3.  **Manually Verify the New Output**: Go into the `onthefly_*` directory.
    Inspect the generated files. Are they correct? Do they reflect the changes
    you intended? This is the most critical step.

4.  **Update the Reference**: If the new output is correct, run the test again,
    this time setting `update_reference=True`.
    .. code-block:: python
       pikobs.unittest_timeserie("/path/to/existing/reference_dir", update_reference=True)
    This will delete the old reference and replace it with your newly-verified
    output. The test will pass and issue a warning that the reference was updated.

5.  **Commit**: Commit both your code changes and the updated reference files
    to version control together. They are now the new "ground truth".

Usage via `python -c`
---------------------

.. code-block:: bash

   # Run a standard regression test
   python -c "import pikobs; pikobs.unittest_timeserie('/path/to/ref')"

   # Run a test and update the reference (Workflow Step 4)
   python -c "import pikobs; pikobs.unittest_timeserie('/path/to/ref', update_reference=True)"

.. warning::
   The validity of this testing system depends entirely on the **correctness
   of the reference files**. It is the user's responsibility to **manually verify**
   any new or updated reference data.

"""
# pikobs/unittest/test_pikobs.py

"""
PikObs Unified Testing Suite
===========================

This module provides a unified, automated framework for functional testing of 
PikObs data generation and analysis tools (e.g., "timeserie", "scatter", etc.).

Its primary goal is to ensure that changes to the codebase do not introduce
unexpected regressions in the output (files, databases, images).

Core Concept: Reference-Based Testing
--------------------------------------

The methodology is based on comparing the results generated by a current run 
("on-the-fly") against a set of pre-validated, stored results known as the 
"reference" or "golden" dataset.

- If the current output is identical to the reference, the test **PASSES**.
- If there are any differences, the test **FAILS**, alerting the developer.

Workflows
---------

There are two primary workflows for using this test suite: creating the initial
reference, and updating it during development.

**A. Bootstrap Workflow (First-Time Run)**

1.  **Run the Test**: Execute the test function pointing to a reference directory
    that does not yet exist.
    .. code-block:: python
       import pikobs
       pikobs.unittest_timeserie("/path/to/new/reference_dir")
2.  **Auto-Population**: The script runs the PikObs tool and, finding no reference,
    it automatically creates the directory and populates it with the new output.
3.  **Manual Verification**: The test **PASSES** with a warning. You **must** now
    manually inspect every file in the new reference directory to ensure it is
    100% correct. Once verified, commit these files to version control.

**B. Recommended Development Workflow (Updating an Existing Reference)**

This is the standard process when you intentionally change the code in a way
that affects the output.

1.  **Modify Your Code**: Make your changes to `pikobs.timeserie`, fix a bug, or
    add a new feature.

2.  **Run the Test Normally**: Run the test without any special flags.
    .. code-block:: python
       pikobs.unittest_timeserie("/path/to/existing/reference_dir")
    The test **is expected to FAIL**. This is good; it confirms your code
    change had an effect. The new, differing output is saved in the temporary
    `onthefly_*` directory.

3.  **Manually Verify the New Output**: Go into the `onthefly_*` directory.
    Inspect the generated files. Are they correct? Do they reflect the changes
    you intended? This is the most critical step.

4.  **Update the Reference**: If the new output is correct, run the test again,
    this time setting `update_reference=True`.
    .. code-block:: python
       pikobs.unittest_timeserie("/path/to/existing/reference_dir", update_reference=True)
    This will delete the old reference and replace it with your newly-verified
    output. The test will pass and issue a warning that the reference was updated.

5.  **Commit**: Commit both your code changes and the updated reference files
    to version control together. They are now the new "ground truth".

Usage via `python -c`
---------------------

.. code-block:: bash

   # Run a standard regression test
   python -c "import pikobs; pikobs.unittest_timeserie('/path/to/ref')"

   # Run a test and update the reference (Workflow Step 4)
   python -c "import pikobs; pikobs.unittest_timeserie('/path/to/ref', update_reference=True)"

.. warning::
   The validity of this testing system depends entirely on the **correctness
   of the reference files**. It is the user's responsibility to **manually verify**
   any new or updated reference data.

"""
import unittest
import subprocess
import os
import sqlite3
import numpy as np
from PIL import Image, ImageChops
import shutil
import warnings
import argparse

def call_pikobs_module(func_name, kwargs):
    """Executes a pikobs function using subprocess."""
    print(f"[*] Building command for pikobs.{func_name}...")
    arg_call = f"import pikobs; pikobs.{func_name}.arg_call()"
    command = ["python", "-c", arg_call]
    for k, v in kwargs.items():
        command.append(f"--{k}")
        command.extend(str(v).split())
    print(f"[*] Executing command in subprocess: {' '.join(command)}")
    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"[!] Subprocess execution failed with return code {result.returncode}")
        raise RuntimeError(f"Execution failed:\n{result.stderr}")
    print(f"[*] Subprocess executed successfully.")
    return result.stdout, result.stderr

def files_identical(ref_file, gen_file):
    """Routes the comparison to the specific file type checker."""
    if ref_file.endswith('.db'):
        return sqlite_files_are_identical(ref_file, gen_file)
    if ref_file.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif')):
        return images_are_identical(ref_file, gen_file)
    with open(ref_file, 'rb') as f1, open(gen_file, 'rb') as f2:
        return f1.read() == f2.read()

def sqlite_files_are_identical(file1, file2):
    """Compares tables and row data of two SQLite databases."""
    print(f"    -> Comparing SQLite databases: {os.path.basename(file1)}")
    with sqlite3.connect(file1) as conn1, sqlite3.connect(file2) as conn2:
        cursor1, cursor2 = conn1.cursor(), conn2.cursor()
        cursor1.execute("SELECT name FROM sqlite_master WHERE type='table';")
        tables1 = set(row[0] for row in cursor1.fetchall())
        cursor2.execute("SELECT name FROM sqlite_master WHERE type='table';")
        tables2 = set(row[0] for row in cursor2.fetchall())
        if tables1 != tables2:
            print(f"    [!] Database tables mismatch in {os.path.basename(file1)}")
            return False
        for name in tables1:
            cursor1.execute(f"SELECT * FROM `{name}` ORDER BY 1;")
            rows1 = sorted(cursor1.fetchall())
            cursor2.execute(f"SELECT * FROM `{name}` ORDER BY 1;")
            rows2 = sorted(cursor2.fetchall())
            if rows1 != rows2:
                print(f"    [!] Data mismatch in table '{name}' of {os.path.basename(file1)}")
                return False
    return True

def images_are_identical(img1_path, img2_path):
    """Compares two images pixel by pixel."""
    print(f"    -> Comparing images: {os.path.basename(img1_path)}")
    img1 = Image.open(img1_path)
    img2 = Image.open(img2_path)
    if img1.size != img2.size or img1.mode != img2.mode:
        print(f"    [!] Image size or mode mismatch in {os.path.basename(img1_path)}")
        return False
    diff = ImageChops.difference(img1, img2)
    if diff.getbbox():
        print(f"    [!] Pixel content mismatch in image {os.path.basename(img1_path)}")
        return False
    return True

def files_exist_and_are_identical(reference_dir, generated_dir):
    """
    Checks if the file structure and content are identical between two directories.
    Fails if files are missing, extra, or have different content.
    """
    ref_files = set()
    for root, _, files in os.walk(reference_dir):
        for name in files:
            ref_files.add(os.path.relpath(os.path.join(root, name), reference_dir))

    gen_files = set()
    for root, _, files in os.walk(generated_dir):
        for name in files:
            gen_files.add(os.path.relpath(os.path.join(root, name), generated_dir))
            
    if ref_files != gen_files:
        print("[!] File structure mismatch detected.")
        missing_in_gen = ref_files - gen_files
        extra_in_gen = gen_files - ref_files
        
        if missing_in_gen:
            print(f"    - Missing files in output (expected but not found): {sorted(list(missing_in_gen))}")
        if extra_in_gen:
            print(f"    - Extra files in output (not present in reference): {sorted(list(extra_in_gen))}")
        return False

    for rel_path in sorted(list(ref_files)):
        ref_file_path = os.path.join(reference_dir, rel_path)
        gen_file_path = os.path.join(generated_dir, rel_path)
        if not files_identical(ref_file_path, gen_file_path):
            return False # The specific error is printed inside files_identical
            
    return True

class PikObsFileComparisonTest(unittest.TestCase):
    func_name = None      
    func_args = None      
    reference_dir = None  
    output_dir = None     
    update_reference = False

    def setUp(self):
        print("\n" + "="*50)
        print(f"[*] Setting up test for: {self.func_name}")
        if not all([self.func_name, self.func_args, self.reference_dir, self.output_dir]):
            self.skipTest("Test not correctly parameterized")
        if os.path.exists(self.output_dir):
            print(f"[*] Cleaning up previous working directory: {self.output_dir}")
            shutil.rmtree(self.output_dir)

    def test_pikobs_call_files_identical(self):
        try:
            # 1. Execute PikObs and generate temporary data
            print("[*] Starting PikObs execution...")
            call_pikobs_module(self.func_name, self.func_args)

            ref_dir = self.reference_dir
            out_dir = self.output_dir
            
            if not os.path.exists(out_dir) or not os.listdir(out_dir):
                self.fail("PikObs execution finished but produced no output files.")

            # --- CORRECTED AND STRICTER LOGIC ---
            
            # CASE 1: Initial creation (Bootstrap) if the reference directory does NOT exist.
            if not os.path.exists(ref_dir):
                print(f"[*] Reference directory not found. Populating for the first time: {ref_dir}")
                shutil.copytree(out_dir, ref_dir)
                warning_msg = (
                    f"\n[WARNING] New reference data has been created in: {ref_dir}\n"
                    "Please manually review the results to ensure they are correct before committing them."
                )
                warnings.warn(warning_msg)
                print(warning_msg)
                print("[*] Skipping comparison for this initial run. Test passes.")
                return # Test passes

            # CASE 2: Forced update if update_reference is True.
            if self.update_reference:
                print(f"[*] update_reference flag is set. Overwriting reference directory: {ref_dir}")
                shutil.rmtree(ref_dir)
                shutil.copytree(out_dir, ref_dir)
                warning_msg = (
                    f"\n[WARNING] Reference data has been updated in: {ref_dir}\n"
                    "Please manually review the results before committing them."
                )
                warnings.warn(warning_msg)
                print(warning_msg)
                print("[*] Skipping comparison. Test passes.")
                return # Test passes

            # CASE 3: Strict comparison in all other cases.
            print("[*] Reference directory exists. Starting strict file-by-file comparison...")
            self.assertTrue(
                files_exist_and_are_identical(ref_dir, out_dir),
                "FAIL: The generated files do not match the reference files. See details above."
            )
            print("[*] All files and their contents matched perfectly. Test PASSED.")
            
        except RuntimeError as e:
            print(f"[!] Test failed due to a runtime error during PikObs execution.")
            self.fail(f"Call to pikobs.{self.func_name} failed: {e}")

def run_pikobs_unittest(func_name, func_args, ref_dir, out_dir, update_reference=False):
    """Helper to configure and run the generic test suite."""
    print(f"[*] Initializing Test Runner for: {func_name}")
    print(f"    - Reference path: {ref_dir}")
    print(f"    - Work path (output): {out_dir}")
    if update_reference:
        print("    - UPDATE MODE: Reference will be overwritten.")
    
    PikObsFileComparisonTest.func_name = func_name
    PikObsFileComparisonTest.func_args = func_args
    PikObsFileComparisonTest.reference_dir = ref_dir
    PikObsFileComparisonTest.output_dir = out_dir
    PikObsFileComparisonTest.update_reference = update_reference
    
    suite = unittest.TestLoader().loadTestsFromTestCase(PikObsFileComparisonTest)
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

def unittest_timeserie(reference_path, update_reference=False):
    """Run the unified PikObs tests for timeserie functionality."""
    print(f"\n[*] --- Starting unittest_timeserie ---")
    ref_path_abs = os.path.abspath(reference_path)
    base_dir = os.path.dirname(ref_path_abs)
    work_path = os.path.join(base_dir, "onthefly_timeserie")

    func_args = {
        "path_control_files": "/home/dlo001/sites8/TESTg0/",
        "control_name": "g0",
        "path_experience_files": "/home/dlo001/sites8/TESTg2/",
        "experience_name": "g2",
        "pathwork": work_path,
        "datestart": "2026031000",
        "dateend": "2026031200",
        "region": "Monde PoleNord",
        "family": "to_amsua_allsky",
        "flags_criteria": "assimilee",
        "fonction": "oma omp",
        "id_stn": "all",
        "channel": "all",
        "n_cpus": "40"
    }
    
    run_pikobs_unittest("timeserie", func_args, ref_path_abs, work_path, update_reference)

def unittest_scatter(reference_path, update_reference=False):
    """Run the unified PikObs tests for scatter functionality."""
    print(f"\n[*] --- Starting unittest_scatter ---")
    ref_path_abs = os.path.abspath(reference_path)
    base_dir = os.path.dirname(ref_path_abs)
    work_path = os.path.join(base_dir, "onthefly_scatter")

    func_args = {
        "path_control_files": "/home/dlo001/sites8/TESTg0/",
        "control_name": "g0",
        "path_experience_files": "/home/dlo001/sites8/TESTg2/",
        "experience_name": "g2",
        "pathwork": work_path,
        "datestart": "2026031000",
        "dateend": "2026031200",
        "region": "Monde PoleNord",
        "family": "to_amsua_allsky",
        "flags_criteria": "assimilee",
        "fonction": "oma omp",
        "id_stn": "all",
        "channel": "all",
        "n_cpus": "40"
    }
    
    run_pikobs_unittest("scatter", func_args, ref_path_abs, work_path, update_reference)

# This block allows direct execution for debugging the test script itself,
# but the primary intended use is via module import.
if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description="PikObs Unified Testing Suite (Direct Runner).",
        epilog="This is for debugging the test script itself. For normal use, import and call the test functions.",
        formatter_class=argparse.RawTextHelpFormatter
    )
    parser.add_argument("test_type", choices=['timeserie', 'scatter'], help="The type of test to run.")
    parser.add_argument("reference_path", help="Path to the directory containing reference files.")
    parser.add_argument(
        "--update-reference",
        action="store_true",
        help="If set, overwrites the reference directory with the new output instead of comparing."
    )
    args = parser.parse_args()

    if args.test_type == "timeserie":
        unittest_timeserie(args.reference_path, args.update_reference)
    elif args.test_type == "scatter":
        unittest_scatter(args.reference_path, args.update_reference)
